From 2c90d991884b5734c530f09ee0845b4f4f40a9bd Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 7 Jan 2026 10:23:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=92=89=E9=92=89?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=86=85=E5=85=8D=E7=99=BB=20-=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8topapi/v2/user/getuserinfo=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designhub/service/DingTalkService.java | 46 +++++++ .../com/designhub/service/UserService.java | 127 +++++++++++++++++- frontend/src/stores/auth.ts | 4 +- frontend/src/views/Login.vue | 4 +- 4 files changed, 176 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/designhub/service/DingTalkService.java b/backend/src/main/java/com/designhub/service/DingTalkService.java index 167c4d1..1e760a1 100644 --- a/backend/src/main/java/com/designhub/service/DingTalkService.java +++ b/backend/src/main/java/com/designhub/service/DingTalkService.java @@ -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 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 request = new HashMap<>(); + request.put("code", authCode); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity> entity = new HttpEntity<>(request, headers); + + ResponseEntity response = restTemplate.postForEntity(url, entity, Map.class); + Map body = response.getBody(); + + log.info("应用内免登获取用户信息响应: {}", body); + + if (body != null && Integer.valueOf(0).equals(body.get("errcode"))) { + Map result = (Map) 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 diff --git a/backend/src/main/java/com/designhub/service/UserService.java b/backend/src/main/java/com/designhub/service/UserService.java index 23a286f..d5b5cbc 100644 --- a/backend/src/main/java/com/designhub/service/UserService.java +++ b/backend/src/main/java/com/designhub/service/UserService.java @@ -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 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 detailedInfo = dingTalkService.getDetailedUserInfo(dingtalkUserId); + List> dingDepartments = new ArrayList<>(); + String avatarUrl = null; + + if (detailedInfo != null) { + avatarUrl = (String) detailedInfo.get("avatar"); + dingDepartments = (List>) 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() + .eq(User::getEmail, email) + ); + + if (user == null) { + // 首次登录:创建用户 + log.info("钉钉用户首次免登录,创建账号: email={}", email); + + List 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() + .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登录功能尚未开放"); diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts index b7d6dc0..9e33bee 100644 --- a/frontend/src/stores/auth.ts +++ b/frontend/src/stores/auth.ts @@ -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 diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index fbadac2..6999eca 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -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('/')