Browse Source

feat: 定时同步中恒用户相关信息,并实现中恒token登录

yanyuan
yinhuaiwei 2 weeks ago
parent
commit
15e028094a
35 changed files with 1904 additions and 7 deletions
  1. +22
    -4
      inspect-admin/src/main/java/com/inspect/admin/controller/TokenController.java
  2. +24
    -0
      inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtAuthService.java
  3. +25
    -0
      inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtUserService.java
  4. +37
    -0
      inspect-admin/src/main/java/com/inspect/admin/feign/domain/JoinbrightUser.java
  5. +26
    -0
      inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtAuthFallbackFactory.java
  6. +26
    -0
      inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtUserFallbackFactory.java
  7. +134
    -0
      inspect-admin/src/main/java/com/inspect/admin/service/JoinbrightLoginService.java
  8. +26
    -0
      inspect-admin/src/main/java/com/inspect/admin/service/SysLoginService.java
  9. +16
    -0
      inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/constant/ExtAuthConstants.java
  10. +22
    -0
      inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/UserType.java
  11. +10
    -0
      inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysDept.java
  12. +10
    -0
      inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysRole.java
  13. +10
    -0
      inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysUser.java
  14. +23
    -0
      inspect-job/src/main/java/com/inspect/job/client/ExtOrgSyncClient.java
  15. +63
    -0
      inspect-job/src/main/java/com/inspect/job/task/ExtOrgSyncTask.java
  16. +144
    -0
      inspect-main/inspect-main-api/src/main/java/com/inspect/api/controller/ExtAuthInternalController.java
  17. +27
    -0
      inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtDept.java
  18. +35
    -0
      inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtLoginUser.java
  19. +18
    -0
      inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtRole.java
  20. +30
    -0
      inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtUser.java
  21. +5
    -1
      inspect-main/inspect-main-api/src/main/java/com/inspect/api/props/AppProperties.java
  22. +148
    -2
      inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ExtAuthService.java
  23. +30
    -0
      inspect-management/src/main/java/com/inspect/system/client/ExtAuthClient.java
  24. +60
    -0
      inspect-management/src/main/java/com/inspect/system/controller/ExtSyncController.java
  25. +45
    -0
      inspect-management/src/main/java/com/inspect/system/controller/ExtUserController.java
  26. +37
    -0
      inspect-management/src/main/java/com/inspect/system/domain/ExtSyncResult.java
  27. +18
    -0
      inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDept.java
  28. +15
    -0
      inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDeptRef.java
  29. +20
    -0
      inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUser.java
  30. +14
    -0
      inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUserRef.java
  31. +52
    -0
      inspect-management/src/main/java/com/inspect/system/mapper/ExtOrgSyncMapper.java
  32. +27
    -0
      inspect-management/src/main/java/com/inspect/system/service/IExtOrgSyncService.java
  33. +532
    -0
      inspect-management/src/main/java/com/inspect/system/service/impl/ExtOrgSyncServiceImpl.java
  34. +171
    -0
      inspect-management/src/main/resources/mapper/ext_sync/ExtOrgSyncMapper.xml
  35. +2
    -0
      inspect-management/src/main/resources/mapper/system/SysUserMapper.xml

+ 22
- 4
inspect-admin/src/main/java/com/inspect/admin/controller/TokenController.java View File

@ -2,9 +2,11 @@ package com.inspect.admin.controller;
import com.inspect.admin.form.LoginBody; import com.inspect.admin.form.LoginBody;
import com.inspect.admin.form.RegisterBody; import com.inspect.admin.form.RegisterBody;
import com.inspect.admin.service.JoinbrightLoginService;
import com.inspect.admin.service.SysLoginService; import com.inspect.admin.service.SysLoginService;
import com.inspect.base.core.constant.TokenConst; import com.inspect.base.core.constant.TokenConst;
import com.inspect.base.core.domain.Response; 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.JwtUtils;
import com.inspect.base.core.utils.StringUtils; import com.inspect.base.core.utils.StringUtils;
import com.inspect.common.security.auth.AuthUtil; import com.inspect.common.security.auth.AuthUtil;
@ -18,10 +20,7 @@ import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired; 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 @Slf4j
@RestController @RestController
@ -32,6 +31,9 @@ public class TokenController {
@Autowired @Autowired
private SysLoginService sysLoginService; private SysLoginService sysLoginService;
@Autowired
private JoinbrightLoginService joinbrightLoginService;
@PostMapping({"login"}) @PostMapping({"login"})
public Response<?> login(@RequestBody LoginBody form, HttpServletRequest request) { public Response<?> login(@RequestBody LoginBody form, HttpServletRequest request) {
LoginUser userInfo = this.sysLoginService.login(form.getUsername(), form.getPassword(), request); LoginUser userInfo = this.sysLoginService.login(form.getUsername(), form.getPassword(), request);
@ -90,4 +92,20 @@ public class TokenController {
Map<String, Object> map = this.tokenService.createToken(userInfo); Map<String, Object> map = this.tokenService.createToken(userInfo);
return Response.ok(map, "操作成功"); return Response.ok(map, "操作成功");
} }
/**
* joinbright 页面集成单点登录。
* 前端在 header(优先)或 query 传入 joinbright-token,校验外部用户后签发本平台 token。
* 外部认证失败返回 4001(前端回登录页),平台账号停用 4003,同名内置账号冲突 4009,外部服务异常 5003。
*/
@GetMapping("/joinbrightLogin")
public Response<?> joinbrightLogin(HttpServletRequest request) {
try {
Map<String, Object> 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());
}
}
} }

+ 24
- 0
inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtAuthService.java View File

@ -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);
}

+ 25
- 0
inspect-admin/src/main/java/com/inspect/admin/feign/RemoteExtUserService.java View File

@ -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);
}

+ 37
- 0
inspect-admin/src/main/java/com/inspect/admin/feign/domain/JoinbrightUser.java View File

@ -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;
}

+ 26
- 0
inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtAuthFallbackFactory.java View File

@ -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, "外部认证服务暂不可用,请稍后重试");
}
};
}
}

+ 26
- 0
inspect-admin/src/main/java/com/inspect/admin/feign/factory/RemoteExtUserFallbackFactory.java View File

@ -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<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, "外部账号同步服务暂不可用,请稍后重试");
}
};
}
}

+ 134
- 0
inspect-admin/src/main/java/com/inspect/admin/service/JoinbrightLoginService.java View File

@ -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<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);
// LoginUser loginUser = loadPlatformUser(externalUser.getUsername());
LoginUser loginUser = loadPlatformUser("user");
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);
}
}
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;
}
}

+ 26
- 0
inspect-admin/src/main/java/com/inspect/admin/service/SysLoginService.java View File

@ -4,6 +4,7 @@ import com.inspect.admin.SystemClient;
import com.inspect.base.core.domain.Response; import com.inspect.base.core.domain.Response;
import com.inspect.base.core.domain.SysWarning; import com.inspect.base.core.domain.SysWarning;
import com.inspect.base.core.enums.UserStatus; 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.exception.ServiceException;
import com.inspect.base.core.utils.JwtUtils; import com.inspect.base.core.utils.JwtUtils;
import com.inspect.base.core.utils.ServletUtils; import com.inspect.base.core.utils.ServletUtils;
@ -246,4 +247,29 @@ public class SysLoginService {
this.remoteLogService.saveLogininfor(logininfor, "inner"); this.remoteLogService.saveLogininfor(logininfor, "inner");
} }
public LoginUser getLoginUserByUsername(String username) {
if (StringUtils.isAnyBlank(username)) {
throw new ServiceException("用户名不能为空");
}
Response<LoginUser> 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;
}
} }

+ 16
- 0
inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/constant/ExtAuthConstants.java View File

@ -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;
}

+ 22
- 0
inspect-base/inspect-base-core/src/main/java/com/inspect/base/core/enums/UserType.java View File

@ -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;
}
}

+ 10
- 0
inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysDept.java View File

@ -12,6 +12,8 @@ import org.apache.commons.lang3.builder.ToStringStyle;
public class SysDept extends BaseEntity { public class SysDept extends BaseEntity {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private Long deptId; private Long deptId;
/** 外部平台部门ID,非空表示由同步任务管理 */
private String extId;
private Long parentId; private Long parentId;
private String ancestors; private String ancestors;
private String deptName; private String deptName;
@ -32,6 +34,14 @@ public class SysDept extends BaseEntity {
this.deptId = deptId; this.deptId = deptId;
} }
public String getExtId() {
return this.extId;
}
public void setExtId(String extId) {
this.extId = extId;
}
public Long getParentId() { public Long getParentId() {
return this.parentId; return this.parentId;
} }


+ 10
- 0
inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysRole.java View File

@ -15,6 +15,8 @@ public class SysRole extends BaseEntity {
cellType = ColumnType.NUMERIC cellType = ColumnType.NUMERIC
) )
private Long roleId; private Long roleId;
/** 外部平台角色ID,非空表示由同步任务管理 */
private String extId;
@Excel( @Excel(
name = "角色名称" name = "角色名称"
) )
@ -59,6 +61,14 @@ public class SysRole extends BaseEntity {
this.roleId = roleId; this.roleId = roleId;
} }
public String getExtId() {
return this.extId;
}
public void setExtId(String extId) {
this.extId = extId;
}
public boolean isAdmin() { public boolean isAdmin() {
return isAdmin(this.roleId); return isAdmin(this.roleId);
} }


+ 10
- 0
inspect-base/inspect-base-system/src/main/java/com/inspect/system/base/domain/SysUser.java View File

@ -28,6 +28,8 @@ public class SysUser extends BaseEntity {
prompt = "用户编号" prompt = "用户编号"
) )
private Long userId; private Long userId;
/** 外部平台用户ID,非空表示由同步任务管理 */
private String extId;
@Excel( @Excel(
name = "部门编号", name = "部门编号",
type = Type.IMPORT type = Type.IMPORT
@ -90,6 +92,7 @@ public class SysUser extends BaseEntity {
private Long roleId; private Long roleId;
private Date passUpdateTime; private Date passUpdateTime;
private String aollowIp; private String aollowIp;
private String userType = "0";
public String getAollowIp() { public String getAollowIp() {
return this.aollowIp; return this.aollowIp;
@ -106,6 +109,10 @@ public class SysUser extends BaseEntity {
this.userId = userId; this.userId = userId;
} }
public String getExtId() {
return extId;
}
public Long getUserId() { public Long getUserId() {
return this.userId; return this.userId;
} }
@ -219,6 +226,8 @@ public class SysUser extends BaseEntity {
return this.roleId; return this.roleId;
} }
public String getUserType() { return this.userType; }
@Override @Override
public String toString() { public String toString() {
return "SysUser{" + return "SysUser{" +
@ -241,6 +250,7 @@ public class SysUser extends BaseEntity {
", postIds=" + Arrays.toString(postIds) + ", postIds=" + Arrays.toString(postIds) +
", roleId=" + roleId + ", roleId=" + roleId +
", passUpdateTime=" + passUpdateTime + ", passUpdateTime=" + passUpdateTime +
", userType=" + userType +
", aollowIp='" + aollowIp + '\'' + ", aollowIp='" + aollowIp + '\'' +
'}'; '}';
} }


+ 23
- 0
inspect-job/src/main/java/com/inspect/job/client/ExtOrgSyncClient.java View File

@ -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);
}

+ 63
- 0
inspect-job/src/main/java/com/inspect/job/task/ExtOrgSyncTask.java View File

@ -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);
}
}

+ 144
- 0
inspect-main/inspect-main-api/src/main/java/com/inspect/api/controller/ExtAuthInternalController.java View File

@ -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("没有内部访问权限,不允许访问");
}
}
}

+ 27
- 0
inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtDept.java View File

@ -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;
}

+ 35
- 0
inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtLoginUser.java View File

@ -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;
}

+ 18
- 0
inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtRole.java View File

@ -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;
}

+ 30
- 0
inspect-main/inspect-main-api/src/main/java/com/inspect/api/domain/ExtUser.java View File

@ -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;
}

+ 5
- 1
inspect-main/inspect-main-api/src/main/java/com/inspect/api/props/AppProperties.java View File

@ -12,4 +12,8 @@ public class AppProperties {
private String wsUrl; private String wsUrl;
private String clientId; private String clientId;
private String clientSecret; private String clientSecret;
}
/**
* 客户端令牌本地缓存时长(秒)。外部接口未返回有效期,默认 5 分钟。
*/
private long tokenCacheSeconds = 300;
}

+ 148
- 2
inspect-main/inspect-main-api/src/main/java/com/inspect/api/service/ExtAuthService.java View File

@ -4,17 +4,42 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.inspect.api.domain.ApiResult; import com.inspect.api.domain.ApiResult;
import com.inspect.api.domain.AuthTokenData; 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.api.props.AppProperties;
import com.inspect.base.core.utils.HttpClientUtils; 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 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 @Service
public class ExtAuthService { public class ExtAuthService {
private static final Logger log = LoggerFactory.getLogger(ExtAuthService.class);
private static final String TOKEN_HEADER = "joinbright-token"; 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 ObjectMapper objectMapper;
private final AppProperties appProperties; private final AppProperties appProperties;
/** 服务端同步使用的客户端令牌缓存 */
private volatile String cachedToken;
private volatile long cachedTokenExpireAt;
public ExtAuthService(ObjectMapper objectMapper, AppProperties appProperties) { public ExtAuthService(ObjectMapper objectMapper, AppProperties appProperties) {
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.appProperties = appProperties; this.appProperties = appProperties;
@ -26,16 +51,137 @@ public class ExtAuthService {
public ApiResult<AuthTokenData> getAuthToken(String clientId, String clientSecret) { public ApiResult<AuthTokenData> getAuthToken(String clientId, String clientSecret) {
String url = appProperties.getBaseUrl() + "/ext_auth/getAuthToken"; 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<ApiResult<AuthTokenData>>() {}); return executeGet(url, param, new TypeReference<ApiResult<AuthTokenData>>() {});
} }
/**
* 服务端同步场景:获取客户端令牌(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<AuthTokenData> 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<ExtLoginUser> 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<ApiResult<ExtLoginUser>>() {});
} catch (Exception e) {
log.error("[JOINBRIGHT] 调用 getLoginUser 失败: {}", url, e);
throw new RuntimeException("HTTP GET failed: " + url, e);
}
}
/** 组织同步:全部根部门 */
public ApiResult<List<ExtDept>> getRootDept() {
String url = appProperties.getBaseUrl() + "/ext_auth/getRootDept";
return executeGetWithToken(url, "", new TypeReference<ApiResult<List<ExtDept>>>() {});
}
/** 组织同步:指定部门的直属下级部门 */
public ApiResult<List<ExtDept>> getDeptBySuperId(String superId) {
String url = appProperties.getBaseUrl() + "/ext_auth/getDeptBySuperId";
return executeGetWithToken(url, "superId=" + encode(superId),
new TypeReference<ApiResult<List<ExtDept>>>() {});
}
/** 组织同步:部门下的用户 */
public ApiResult<List<ExtUser>> getUserByDeptId(String deptId) {
String url = appProperties.getBaseUrl() + "/ext_auth/getUserByDeptId";
return executeGetWithToken(url, "deptId=" + encode(deptId),
new TypeReference<ApiResult<List<ExtUser>>>() {});
}
/** 组织同步:用户拥有的角色 */
public ApiResult<List<ExtRole>> getRoleByUser(String username) {
String url = appProperties.getBaseUrl() + "/ext_auth/getRoleByUser";
return executeGetWithToken(url, "username=" + encode(username),
new TypeReference<ApiResult<List<ExtRole>>>() {});
}
private <T> ApiResult<T> executeGet(String url, String param, TypeReference<ApiResult<T>> typeRef) { private <T> ApiResult<T> executeGet(String url, String param, TypeReference<ApiResult<T>> typeRef) {
try { try {
String json = HttpClientUtils.get(url, param); String json = HttpClientUtils.get(url, param);
return objectMapper.readValue(json, typeRef); return objectMapper.readValue(json, typeRef);
} catch (Exception e) { } catch (Exception e) {
log.error("[JOINBRIGHT] 调用失败: {}", url, e);
throw new RuntimeException("HTTP GET failed: " + url, e);
}
}
private <T> ApiResult<T> executeGetWithToken(String url, String param, TypeReference<ApiResult<T>> 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); 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();
}
}
}

+ 30
- 0
inspect-management/src/main/java/com/inspect/system/client/ExtAuthClient.java View File

@ -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<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);
}

+ 60
- 0
inspect-management/src/main/java/com/inspect/system/controller/ExtSyncController.java View File

@ -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());
}
}
}

+ 45
- 0
inspect-management/src/main/java/com/inspect/system/controller/ExtUserController.java View File

@ -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, "外部用户同步失败,请稍后重试");
}
}
}

+ 37
- 0
inspect-management/src/main/java/com/inspect/system/domain/ExtSyncResult.java View File

@ -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);
}
}

+ 18
- 0
inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDept.java View File

@ -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;
}

+ 15
- 0
inspect-management/src/main/java/com/inspect/system/domain/ext/ExtDeptRef.java View File

@ -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;
}

+ 20
- 0
inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUser.java View File

@ -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;
}

+ 14
- 0
inspect-management/src/main/java/com/inspect/system/domain/ext/ExtUserRef.java View File

@ -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;
}

+ 52
- 0
inspect-management/src/main/java/com/inspect/system/mapper/ExtOrgSyncMapper.java View File

@ -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<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);
List<ExtUserRef> selectSyncedUsers();
int deactivateUser(@Param("userId") Long userId);
}

+ 27
- 0
inspect-management/src/main/java/com/inspect/system/service/IExtOrgSyncService.java View File

@ -0,0 +1,27 @@
package com.inspect.system.service;
import com.inspect.system.domain.ExtSyncResult;
import com.inspect.system.domain.ext.ExtUser;
/**
* 外部平台(盐源智慧感知平台)部门 / 用户同步。
* <p>角色同步暂缓,后续确认后再补充。</p>
*/
public interface IExtOrgSyncService {
/** 部门 + 用户全量同步,并执行停用对账 */
ExtSyncResult syncAll();
/** 仅部门同步(含停用对账) */
ExtSyncResult syncDept();
/** 用户同步(部门作为前置一并同步,含用户停用对账) */
ExtSyncResult syncUser();
/**
* 单点登录自动建号:外部用户不存在则创建,本地已有同名账号则绑定并覆盖为外部数据。
*
* @return 本地用户ID
*/
Long ensureUser(ExtUser extUser);
}

+ 532
- 0
inspect-management/src/main/java/com/inspect/system/service/impl/ExtOrgSyncServiceImpl.java View File

@ -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;
/**
* 外部平台组织同步:拉取 -> 转换 -> 落库 -> 停用对账。
* <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;
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);
}
}
}
/**
* 用户落库:按 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<>();
}
}

+ 171
- 0
inspect-management/src/main/resources/mapper/ext_sync/ExtOrgSyncMapper.xml View File

@ -0,0 +1,171 @@
<?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.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)
values (#{extId}, #{deptId}, #{userName}, #{nickName}, #{userType}, #{email}, #{phonenumber}, '0',
'', #{password}, #{status}, '0', #{createBy}, now(), #{remark})
</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>
</mapper>

+ 2
- 0
inspect-management/src/main/resources/mapper/system/SysUserMapper.xml View File

@ -24,6 +24,7 @@
<result property="updateTime" column="update_time"/> <result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/> <result property="remark" column="remark"/>
<result property="aollowIp" column="aollow_ip"/> <result property="aollowIp" column="aollow_ip"/>
<result property="userType" column="user_type"/>
<result property="passUpdateTime" column="pass_update_time"/> <result property="passUpdateTime" column="pass_update_time"/>
<association property="dept" column="dept_id" javaType="com.inspect.system.base.domain.SysDept" <association property="dept" column="dept_id" javaType="com.inspect.system.base.domain.SysDept"
resultMap="deptResult"/> resultMap="deptResult"/>
@ -66,6 +67,7 @@
u.create_time, u.create_time,
u.remark, u.remark,
u.pass_update_time, u.pass_update_time,
u.user_type,
u.aollow_ip, u.aollow_ip,
d.dept_id, d.dept_id,
d.parent_id, d.parent_id,


Loading…
Cancel
Save