feat: 支持钉钉应用内免登 - 使用topapi/v2/user/getuserinfo接口
This commit is contained in:
parent
0afd1b7d17
commit
2c90d99188
@ -512,6 +512,52 @@ public class DingTalkService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过临时授权码获取用户信息(应用内免登专用)
|
||||
* 使用 /topapi/v2/user/getuserinfo 接口
|
||||
* 参考:https://open.dingtalk.com/document/orgapp/obtain-the-userid-of-a-user-by-using-the-log-free
|
||||
*/
|
||||
public Map<String, Object> getUserInfoByAuthCode(String authCode) {
|
||||
DingTalkConfig config = getConfig();
|
||||
if (!isConfigured()) {
|
||||
throw new BusinessException(ResultCode.PARAM_ERROR, "钉钉登录未配置或未启用");
|
||||
}
|
||||
|
||||
String accessToken = getAccessToken(config);
|
||||
|
||||
try {
|
||||
String url = String.format("%s/topapi/v2/user/getuserinfo?access_token=%s",
|
||||
DINGTALK_API_BASE, accessToken);
|
||||
|
||||
Map<String, Object> request = new HashMap<>();
|
||||
request.put("code", authCode);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
|
||||
|
||||
ResponseEntity<Map> response = restTemplate.postForEntity(url, entity, Map.class);
|
||||
Map<String, Object> body = response.getBody();
|
||||
|
||||
log.info("应用内免登获取用户信息响应: {}", body);
|
||||
|
||||
if (body != null && Integer.valueOf(0).equals(body.get("errcode"))) {
|
||||
Map<String, Object> result = (Map<String, Object>) body.get("result");
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
String errMsg = body != null ? String.valueOf(body.get("errmsg")) : "未知错误";
|
||||
throw new BusinessException(ResultCode.DINGTALK_API_ERROR, "获取用户信息失败: " + errMsg);
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("应用内免登获取用户信息失败", e);
|
||||
throw new BusinessException(ResultCode.DINGTALK_API_ERROR, "获取钉钉用户信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户unionId
|
||||
* 参考:https://open.dingtalk.com/document/orgapp/obtain-the-user-information-based-on-the-sns-temporary-authorization
|
||||
|
||||
@ -585,6 +585,125 @@ public class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理钉钉应用内免登
|
||||
* 使用 /topapi/v2/user/getuserinfo 接口
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
private LoginVO handleDingTalkInAppLogin(String authCode) {
|
||||
log.info("开始钉钉应用内免登流程, code: {}...", authCode.substring(0, Math.min(10, authCode.length())));
|
||||
|
||||
try {
|
||||
// 1. 通过授权码直接获取用户信息(包含userid)
|
||||
Map<String, Object> userInfo = dingTalkService.getUserInfoByAuthCode(authCode);
|
||||
String dingtalkUserId = (String) userInfo.get("userid");
|
||||
String unionId = (String) userInfo.get("unionid");
|
||||
String name = (String) userInfo.get("name");
|
||||
|
||||
if (dingtalkUserId == null) {
|
||||
throw new BusinessException(ResultCode.DINGTALK_AUTH_ERROR, "获取钉钉用户ID失败");
|
||||
}
|
||||
|
||||
log.info("钉钉应用内免登用户信息: userid={}, unionId={}, name={}", dingtalkUserId, unionId, name);
|
||||
|
||||
// 2. 获取详细用户信息
|
||||
Map<String, Object> detailedInfo = dingTalkService.getDetailedUserInfo(dingtalkUserId);
|
||||
List<Map<String, Object>> dingDepartments = new ArrayList<>();
|
||||
String avatarUrl = null;
|
||||
|
||||
if (detailedInfo != null) {
|
||||
avatarUrl = (String) detailedInfo.get("avatar");
|
||||
dingDepartments = (List<Map<String, Object>>) detailedInfo.get("departments");
|
||||
if (dingDepartments != null && !dingDepartments.isEmpty()) {
|
||||
String deptNames = dingDepartments.stream()
|
||||
.map(d -> String.valueOf(d.get("name")))
|
||||
.collect(Collectors.joining(", "));
|
||||
log.info("钉钉用户所属部门: {}", deptNames);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 查找或创建用户(使用unionId作为唯一标识)
|
||||
String email = "dingtalk_" + unionId + "@dingtalk.local";
|
||||
User user = userMapper.selectOne(
|
||||
new LambdaQueryWrapper<User>()
|
||||
.eq(User::getEmail, email)
|
||||
);
|
||||
|
||||
if (user == null) {
|
||||
// 首次登录:创建用户
|
||||
log.info("钉钉用户首次免登录,创建账号: email={}", email);
|
||||
|
||||
List<Long> matchedDeptIds = matchSystemDepartments(dingDepartments);
|
||||
|
||||
user = new User();
|
||||
user.setUsername("dd_" + dingtalkUserId);
|
||||
user.setPassword(passwordEncoder.encode("dingtalk_" + System.currentTimeMillis()));
|
||||
user.setRealName(name);
|
||||
user.setEmail(email);
|
||||
user.setAvatar(avatarUrl);
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
if (!matchedDeptIds.isEmpty()) {
|
||||
Department matchedDept = departmentMapper.selectById(matchedDeptIds.get(0));
|
||||
if (matchedDept != null) {
|
||||
user.setDeptId(matchedDept.getId());
|
||||
user.setDeptName(matchedDept.getDeptName());
|
||||
user.setRoleIds("receiving".equals(matchedDept.getDeptType()) ? "5" : "6");
|
||||
user.setStatus(1);
|
||||
}
|
||||
} else {
|
||||
// 尝试使用默认部门
|
||||
Department defaultDept = departmentMapper.selectOne(
|
||||
new LambdaQueryWrapper<Department>()
|
||||
.eq(Department::getStatus, 1)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (defaultDept != null) {
|
||||
user.setDeptId(defaultDept.getId());
|
||||
user.setDeptName(defaultDept.getDeptName());
|
||||
user.setRoleIds("receiving".equals(defaultDept.getDeptType()) ? "5" : "6");
|
||||
user.setStatus(1);
|
||||
} else {
|
||||
user.setStatus(0);
|
||||
}
|
||||
}
|
||||
|
||||
userMapper.insert(user);
|
||||
|
||||
if (!matchedDeptIds.isEmpty()) {
|
||||
setUserDepartments(user.getId(), matchedDeptIds, matchedDeptIds.get(0));
|
||||
}
|
||||
|
||||
log.info("钉钉免登用户创建成功: userId={}", user.getId());
|
||||
} else {
|
||||
// 已有用户:同步更新信息
|
||||
log.info("钉钉免登用户已存在: userId={}", user.getId());
|
||||
syncUserFromDingTalk(user, detailedInfo, dingtalkUserId, dingDepartments);
|
||||
}
|
||||
|
||||
// 4. 生成Token
|
||||
String token = jwtUtil.generateToken(user.getId(), user.getUsername());
|
||||
String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getUsername());
|
||||
|
||||
return LoginVO.builder()
|
||||
.token(token)
|
||||
.refreshToken(refreshToken)
|
||||
.userId(user.getId())
|
||||
.username(user.getUsername())
|
||||
.realName(user.getRealName())
|
||||
.avatar(user.getAvatar())
|
||||
.roleIds(user.getRoleIds())
|
||||
.build();
|
||||
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("钉钉应用内免登失败", e);
|
||||
throw new BusinessException(ResultCode.DINGTALK_AUTH_ERROR, "钉钉免登失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理钉钉OAuth登录
|
||||
*/
|
||||
@ -843,11 +962,17 @@ public class UserService {
|
||||
.roleIds(user.getRoleIds())
|
||||
.build();
|
||||
} else if ("dingtalk".equals(loginType)) {
|
||||
// 钉钉OAuth 2.0登录 - 验证授权码
|
||||
// 钉钉OAuth 2.0登录(扫码登录) - 验证授权码
|
||||
if (loginDTO.getAuthCode() == null || loginDTO.getAuthCode().trim().isEmpty()) {
|
||||
throw new BusinessException(ResultCode.PARAM_ERROR, "钉钉授权码不能为空");
|
||||
}
|
||||
return handleDingTalkLogin(loginDTO.getAuthCode());
|
||||
} else if ("dingtalk_inapp".equals(loginType)) {
|
||||
// 钉钉应用内免登 - 使用不同的API
|
||||
if (loginDTO.getAuthCode() == null || loginDTO.getAuthCode().trim().isEmpty()) {
|
||||
throw new BusinessException(ResultCode.PARAM_ERROR, "钉钉授权码不能为空");
|
||||
}
|
||||
return handleDingTalkInAppLogin(loginDTO.getAuthCode());
|
||||
} else if ("casdoor".equals(loginType)) {
|
||||
// Casdoor登录 - 暂时返回错误,待后续实现
|
||||
throw new BusinessException(ResultCode.PARAM_ERROR, "Casdoor登录功能尚未开放");
|
||||
|
||||
@ -155,11 +155,11 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
/**
|
||||
* 钉钉免登录(使用授权码)
|
||||
*/
|
||||
const loginWithDingTalk = async (authCode: string) => {
|
||||
const loginWithDingTalk = async (authCode: string, isInApp: boolean = false) => {
|
||||
try {
|
||||
const response = await authApi.login({
|
||||
authCode,
|
||||
loginType: 'dingtalk'
|
||||
loginType: isInApp ? 'dingtalk_inapp' : 'dingtalk'
|
||||
})
|
||||
|
||||
const loginData = response.data
|
||||
|
||||
@ -481,8 +481,8 @@ const handleDingTalkAutoLogin = async () => {
|
||||
onSuccess: async (result: { code: string }) => {
|
||||
console.log('获取到免登授权码:', result.code)
|
||||
try {
|
||||
// 4. 使用授权码登录
|
||||
const loginResult = await authStore.loginWithDingTalk(result.code)
|
||||
// 4. 使用授权码登录(应用内免登)
|
||||
const loginResult = await authStore.loginWithDingTalk(result.code, true)
|
||||
if (loginResult.success) {
|
||||
message.success('钉钉免登录成功')
|
||||
router.push('/')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user