feat: 支持钉钉应用内免登 - 使用topapi/v2/user/getuserinfo接口

This commit is contained in:
Your Name 2026-01-07 10:23:04 +08:00
parent 0afd1b7d17
commit 2c90d99188
4 changed files with 176 additions and 5 deletions

View File

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

View File

@ -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登录功能尚未开放");

View File

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

View File

@ -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('/')