Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
2026-02-03 21:41:55 +08:00
53 changed files with 3638 additions and 2 deletions

View File

@@ -0,0 +1,105 @@
package com.vetti.web.controller.hotake;
import com.vetti.common.annotation.Log;
import com.vetti.common.core.controller.BaseController;
import com.vetti.common.core.domain.R;
import com.vetti.common.core.page.TableWebDataInfo;
import com.vetti.common.enums.BusinessType;
import com.vetti.hotake.domain.HotakeAccessibility;
import com.vetti.hotake.service.IHotakeAccessibilityService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户无障碍设置Controller
*
* @author vetti
* @date 2026-02-01
*/
@Api(tags = "用户无障碍设置")
@RestController
@RequestMapping("/hotake/accessibility")
public class HotakeAccessibilityController extends BaseController {
@Autowired
private IHotakeAccessibilityService hotakeAccessibilityService;
/**
* 查询用户无障碍设置列表
*/
@ApiOperation("查询用户无障碍设置列表(分页)")
@GetMapping("/getPagelist")
public TableWebDataInfo<HotakeAccessibility> list(HotakeAccessibility hotakeAccessibility) {
startPage();
hotakeAccessibility.setCreateBy(getUsername());
List<HotakeAccessibility> list = hotakeAccessibilityService.selectHotakeAccessibilityList(hotakeAccessibility);
return getWebDataTable(list);
}
/**
* 查询用户无障碍设置列表
*/
@ApiOperation("查询用户无障碍设置列表(无分页)")
@GetMapping("/getList")
public R<List<HotakeAccessibility>> getList(HotakeAccessibility hotakeAccessibility) {
hotakeAccessibility.setCreateBy(getUsername());
List<HotakeAccessibility> list = hotakeAccessibilityService.selectHotakeAccessibilityList(hotakeAccessibility);
return R.ok(list, "");
}
/**
* 获取用户无障碍设置详细信息
*/
@ApiOperation("获取用户无障碍设置详细信息")
@GetMapping(value = "/{id}")
public R<HotakeAccessibility> getInfo(@ApiParam("设置ID") @PathVariable("id") Integer id) {
return R.ok(hotakeAccessibilityService.selectHotakeAccessibilityById(id), "");
}
/**
* 新增用户无障碍设置
*/
@ApiOperation("新增用户无障碍设置")
@Log(title = "用户无障碍设置", businessType = BusinessType.INSERT)
@PostMapping
public R<HotakeAccessibility> add(@RequestBody HotakeAccessibility hotakeAccessibility) {
return R.ok(hotakeAccessibilityService.insertHotakeAccessibility(hotakeAccessibility));
}
/**
* 修改用户无障碍设置
*/
@ApiOperation("修改用户无障碍设置")
@Log(title = "用户无障碍设置", businessType = BusinessType.UPDATE)
@PutMapping
public R<HotakeAccessibility> edit(@RequestBody HotakeAccessibility hotakeAccessibility) {
return R.ok(hotakeAccessibilityService.updateHotakeAccessibility(hotakeAccessibility));
}
/**
* 删除用户无障碍设置
*/
@ApiOperation("删除用户无障碍设置")
@Log(title = "用户无障碍设置", businessType = BusinessType.DELETE)
@DeleteMapping("/{id}")
public R remove(@PathVariable Integer id) {
return R.ok(hotakeAccessibilityService.deleteHotakeAccessibilityById(id));
}
/**
* 获取当前登录用户的无障碍设置
* 如果不存在则自动创建默认设置
*/
@ApiOperation("获取当前登录用户的无障碍设置")
@GetMapping("/getCurrentUser")
public R<HotakeAccessibility> getCurrentUserAccessibility() {
String username = getUsername();
HotakeAccessibility accessibility = hotakeAccessibilityService.getCurrentUserAccessibility(username);
return R.ok(accessibility, "");
}
}

View File

@@ -0,0 +1,105 @@
package com.vetti.web.controller.hotake;
import com.vetti.common.annotation.Log;
import com.vetti.common.core.controller.BaseController;
import com.vetti.common.core.domain.R;
import com.vetti.common.core.page.TableWebDataInfo;
import com.vetti.common.enums.BusinessType;
import com.vetti.hotake.domain.HotakeNotificationPreferences;
import com.vetti.hotake.service.IHotakeNotificationPreferencesService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户通知偏好设置Controller
*
* @author vetti
* @date 2026-02-01
*/
@Api(tags = "用户通知偏好设置")
@RestController
@RequestMapping("/hotake/notificationPreferences")
public class HotakeNotificationPreferencesController extends BaseController {
@Autowired
private IHotakeNotificationPreferencesService hotakeNotificationPreferencesService;
/**
* 查询用户通知偏好设置列表
*/
@ApiOperation("查询用户通知偏好设置列表(分页)")
@GetMapping("/getPagelist")
public TableWebDataInfo<HotakeNotificationPreferences> list(HotakeNotificationPreferences hotakeNotificationPreferences) {
startPage();
hotakeNotificationPreferences.setCreateBy(getUsername());
List<HotakeNotificationPreferences> list = hotakeNotificationPreferencesService.selectHotakeNotificationPreferencesList(hotakeNotificationPreferences);
return getWebDataTable(list);
}
/**
* 查询用户通知偏好设置列表
*/
@ApiOperation("查询用户通知偏好设置列表(无分页)")
@GetMapping("/getList")
public R<List<HotakeNotificationPreferences>> getList(HotakeNotificationPreferences hotakeNotificationPreferences) {
hotakeNotificationPreferences.setCreateBy(getUsername());
List<HotakeNotificationPreferences> list = hotakeNotificationPreferencesService.selectHotakeNotificationPreferencesList(hotakeNotificationPreferences);
return R.ok(list, "");
}
/**
* 获取用户通知偏好设置详细信息
*/
@ApiOperation("获取用户通知偏好设置详细信息")
@GetMapping(value = "/{id}")
public R<HotakeNotificationPreferences> getInfo(@ApiParam("设置ID") @PathVariable("id") Long id) {
return R.ok(hotakeNotificationPreferencesService.selectHotakeNotificationPreferencesById(id), "");
}
/**
* 新增用户通知偏好设置
*/
@ApiOperation("新增用户通知偏好设置")
@Log(title = "用户通知偏好设置", businessType = BusinessType.INSERT)
@PostMapping
public R<HotakeNotificationPreferences> add(@RequestBody HotakeNotificationPreferences hotakeNotificationPreferences) {
return R.ok(hotakeNotificationPreferencesService.insertHotakeNotificationPreferences(hotakeNotificationPreferences));
}
/**
* 修改用户通知偏好设置
*/
@ApiOperation("修改用户通知偏好设置")
@Log(title = "用户通知偏好设置", businessType = BusinessType.UPDATE)
@PutMapping
public R<HotakeNotificationPreferences> edit(@RequestBody HotakeNotificationPreferences hotakeNotificationPreferences) {
return R.ok(hotakeNotificationPreferencesService.updateHotakeNotificationPreferences(hotakeNotificationPreferences));
}
/**
* 删除用户通知偏好设置
*/
@ApiOperation("删除用户通知偏好设置")
@Log(title = "用户通知偏好设置", businessType = BusinessType.DELETE)
@DeleteMapping("/{id}")
public R remove(@PathVariable Long id) {
return R.ok(hotakeNotificationPreferencesService.deleteHotakeNotificationPreferencesById(id));
}
/**
* 获取当前登录用户的通知偏好设置
* 如果不存在则自动创建默认设置
*/
@ApiOperation("获取当前登录用户的通知偏好设置")
@GetMapping("/getCurrentUser")
public R<HotakeNotificationPreferences> getCurrentUserPreferences() {
String username = getUsername();
HotakeNotificationPreferences preferences = hotakeNotificationPreferencesService.getCurrentUserPreferences(username);
return R.ok(preferences, "");
}
}

View File

@@ -0,0 +1,103 @@
package com.vetti.web.controller.hotake;
import com.vetti.common.annotation.Log;
import com.vetti.common.core.controller.BaseController;
import com.vetti.common.core.domain.R;
import com.vetti.common.core.page.TableWebDataInfo;
import com.vetti.common.enums.BusinessType;
import com.vetti.hotake.domain.HotakeReferenceCheck;
import com.vetti.hotake.domain.dto.VcDto.VcExperienceDto;
import com.vetti.hotake.service.IHotakeReferenceCheckService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 参考检查Controller
*
* @author vetti
* @date 2026-01-29
*/
@Api(tags = "背景调查")
@RestController
@RequestMapping("/hotake/referenceCheck")
public class HotakeReferenceCheckController extends BaseController {
@Autowired
private IHotakeReferenceCheckService hotakeReferenceCheckService;
/**
* 查询参考检查列表
*/
@ApiOperation("查询参考检查列表(分页)")
@GetMapping("/getPagelist")
public TableWebDataInfo<HotakeReferenceCheck> list(HotakeReferenceCheck hotakeReferenceCheck) {
startPage();
hotakeReferenceCheck.setCreateBy(getUsername());
List<HotakeReferenceCheck> list = hotakeReferenceCheckService.selectHotakeReferenceCheckList(hotakeReferenceCheck);
return getWebDataTable(list);
}
/**
* 查询参考检查列表
*/
@ApiOperation("查询参考检查列表(无分页)")
@GetMapping("/getList")
public R<List<HotakeReferenceCheck>> getList(HotakeReferenceCheck hotakeReferenceCheck) {
hotakeReferenceCheck.setCreateBy(getUsername());
List<HotakeReferenceCheck> list = hotakeReferenceCheckService.selectHotakeReferenceCheckList(hotakeReferenceCheck);
return R.ok(list, "");
}
/**
* 获取参考检查详细信息
*/
@ApiOperation("获取参考检查详细信息")
@GetMapping(value = "/{id}")
public R<HotakeReferenceCheck> getInfo(@ApiParam("参考检查ID") @PathVariable("id") Integer id) {
return R.ok(hotakeReferenceCheckService.selectHotakeReferenceCheckById(id), "");
}
/**
* 新增参考检查
*/
@ApiOperation("新增参考检查")
@Log(title = "参考检查", businessType = BusinessType.INSERT)
@PostMapping
public R<HotakeReferenceCheck> add(@RequestBody HotakeReferenceCheck hotakeReferenceCheck) {
return R.ok(hotakeReferenceCheckService.insertHotakeReferenceCheck(hotakeReferenceCheck));
}
/**
* 修改参考检查
*/
@ApiOperation("修改参考检查")
@Log(title = "参考检查", businessType = BusinessType.UPDATE)
@PutMapping
public R<HotakeReferenceCheck> edit(@RequestBody HotakeReferenceCheck hotakeReferenceCheck) {
return R.ok(hotakeReferenceCheckService.updateHotakeReferenceCheck(hotakeReferenceCheck));
}
/**
* 删除参考检查
*/
@ApiOperation("删除参考检查")
@Log(title = "参考检查", businessType = BusinessType.DELETE)
@DeleteMapping("/{id}")
public R remove(@PathVariable Integer id) {
return R.ok(hotakeReferenceCheckService.deleteHotakeReferenceCheckById(id));
}
/**
* 获取当前登录人的工作经验列表
*/
@ApiOperation("获取当前登录人的工作经验列表")
@GetMapping("/getExperienceList")
public R<List<VcExperienceDto>> getExperienceList() {
return R.ok(hotakeReferenceCheckService.getCurrentUserExperienceList(), "");
}
}

View File

@@ -0,0 +1,104 @@
package com.vetti.web.controller.hotake;
import com.vetti.common.annotation.Log;
import com.vetti.common.core.controller.BaseController;
import com.vetti.common.core.domain.R;
import com.vetti.common.core.page.TableWebDataInfo;
import com.vetti.common.enums.BusinessType;
import com.vetti.hotake.domain.HotakeSettingsJob;
import com.vetti.hotake.domain.dto.HotakeSettingsJobDictDto;
import com.vetti.hotake.service.IHotakeSettingsJobService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户首选工作设置Controller
*
* @author wangxiangshun
* @date 2025-11-02
*/
@Api(tags = "用户首选工作设置")
@RestController
@RequestMapping("/hotake/settingsJob")
public class HotakeSettingsJobController extends BaseController {
@Autowired
private IHotakeSettingsJobService hotakeSettingsJobService;
/**
* 查询用户首选工作设置列表
*/
@ApiOperation("查询用户首选工作设置列表(分页)")
@GetMapping("/getPagelist")
public TableWebDataInfo<HotakeSettingsJob> list(HotakeSettingsJob hotakeSettingsJob) {
startPage();
hotakeSettingsJob.setCreateBy(getUsername());
List<HotakeSettingsJob> list = hotakeSettingsJobService.selectHotakeSettingsJobList(hotakeSettingsJob);
return getWebDataTable(list);
}
/**
* 查询用户首选工作设置列表
*/
@ApiOperation("查询用户首选工作设置列表(无分页)")
@GetMapping("/getList")
public R<List<HotakeSettingsJob>> getList(HotakeSettingsJob hotakeSettingsJob) {
hotakeSettingsJob.setCreateBy(getUsername());
List<HotakeSettingsJob> list = hotakeSettingsJobService.selectHotakeSettingsJobList(hotakeSettingsJob);
return R.ok(list, "");
}
/**
* 获取用户首选工作设置字典对照内容
*/
@ApiOperation("获取用户首选工作设置字典对照内容")
@GetMapping("/getDictData")
public R<HotakeSettingsJobDictDto> getDictData() {
HotakeSettingsJobDictDto dictData = hotakeSettingsJobService.getDictData();
return R.ok(dictData, "");
}
/**
* 获取用户首选工作设置详细信息
*/
@ApiOperation("获取用户首选工作设置详细信息")
@GetMapping(value = "/{id}")
public R<HotakeSettingsJob> getInfo(@ApiParam("设置ID") @PathVariable("id") Long id) {
return R.ok(hotakeSettingsJobService.selectHotakeSettingsJobById(id), "");
}
/**
* 新增用户首选工作设置
*/
@ApiOperation("新增用户首选工作设置")
@Log(title = "用户首选工作设置", businessType = BusinessType.INSERT)
@PostMapping
public R<HotakeSettingsJob> add(@RequestBody HotakeSettingsJob hotakeSettingsJob) {
return R.ok(hotakeSettingsJobService.insertHotakeSettingsJob(hotakeSettingsJob));
}
/**
* 修改用户首选工作设置
*/
@ApiOperation("修改用户首选工作设置")
@Log(title = "用户首选工作设置", businessType = BusinessType.UPDATE)
@PutMapping
public R<HotakeSettingsJob> edit(@RequestBody HotakeSettingsJob hotakeSettingsJob) {
return R.ok(hotakeSettingsJobService.updateHotakeSettingsJob(hotakeSettingsJob));
}
/**
* 删除用户首选工作设置
*/
@ApiOperation("删除用户首选工作设置")
@Log(title = "用户首选工作设置", businessType = BusinessType.DELETE)
@DeleteMapping("/{id}")
public R remove(@PathVariable Long id) {
return R.ok(hotakeSettingsJobService.deleteHotakeSettingsJobById(id));
}
}

View File

@@ -0,0 +1,88 @@
package com.vetti.web.controller.hotake;
import com.vetti.common.annotation.Anonymous;
import com.vetti.common.core.domain.AjaxResult;
import com.vetti.common.core.domain.R;
import com.vetti.common.utils.SecurityUtils;
import com.vetti.hotake.domain.HotakeSocialUser;
import com.vetti.hotake.domain.dto.HotakeSocialLoginRequestDto;
import com.vetti.hotake.domain.dto.HotakeSocialLoginResultDto;
import com.vetti.hotake.service.IHotakeSocialUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 社交登录控制器
*
* @author vetti
*/
@Api(tags = "社交登录模块")
@RestController
@RequestMapping("/oauth2")
public class HotakeSocialLoginController {
@Autowired
private IHotakeSocialUserService socialUserService;
/**
* 获取OAuth授权URL
*/
@Anonymous
@ApiOperation("获取OAuth授权URL")
@GetMapping("/authorize/{provider}")
public R<String> getAuthorizationUrl(
@ApiParam(value = "平台类型google/microsoft/linkedin", required = true)
@PathVariable String provider,
@ApiParam(value = "state参数用于防止CSRF攻击")
@RequestParam(required = false) String state) {
String authUrl = socialUserService.getAuthorizationUrl(provider, state);
return R.ok(authUrl, "");
}
/**
* 社交登录回调用code换取token并登录
*/
@Anonymous
@ApiOperation("社交登录")
@PostMapping("/login")
public R<HotakeSocialLoginResultDto> socialLogin(@Validated @RequestBody HotakeSocialLoginRequestDto requestDto) {
HotakeSocialLoginResultDto resultDto = socialUserService.socialLogin(requestDto);
return R.ok(resultDto, "");
}
/**
* 获取当前用户绑定的社交账号列表
*/
@ApiOperation("获取当前用户绑定的社交账号列表")
@GetMapping("/bindList")
public R<List<HotakeSocialUser>> getBindList() {
Long userId = SecurityUtils.getUserId();
List<HotakeSocialUser> list = socialUserService.listByUserId(userId);
// 脱敏处理不返回token等敏感信息
list.forEach(item -> {
item.setAccessToken(null);
item.setRefreshToken(null);
item.setRawUserInfo(null);
});
return R.ok(list, "");
}
/**
* 解绑社交账号
*/
@ApiOperation("解绑社交账号")
@DeleteMapping("/unbind/{provider}")
public AjaxResult unbind(
@ApiParam(value = "平台类型google/microsoft/linkedin", required = true)
@PathVariable String provider) {
Long userId = SecurityUtils.getUserId();
socialUserService.unbind(userId, provider);
return AjaxResult.success("解绑成功");
}
}

View File

@@ -2,9 +2,12 @@ package com.vetti.web.controller.system;
import java.util.*;
import cn.hutool.core.collection.CollectionUtil;
import com.vetti.common.core.domain.R;
import com.vetti.common.core.domain.dto.LoginDto;
import com.vetti.common.utils.MessageUtils;
import com.vetti.hotake.domain.HotakeCvInfo;
import com.vetti.hotake.service.IHotakeCvInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
@@ -52,6 +55,9 @@ public class SysLoginController
@Autowired
private ISysConfigService configService;
@Autowired
private IHotakeCvInfoService cvInfoService;
/**
* 登录方法
*
@@ -64,6 +70,19 @@ public class SysLoginController
{
LoginDto loginDto = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
loginBody.getUuid());
// 如果是候选者,查询是否有简历
if (loginDto.getUser() != null && "candidate".equals(loginDto.getUser().getSysUserType())) {
HotakeCvInfo query = new HotakeCvInfo();
query.setUserId(loginDto.getUserId());
query.setCvFileType("cv"); // 只查询简历类型的文件
List<HotakeCvInfo> cvList = cvInfoService.selectHotakeCvInfoList(query);
// 设置简历状态标识
loginDto.setHasCv(!CollectionUtil.isEmpty(cvList));
loginDto.setCvCount(cvList != null ? cvList.size() : 0);
}
return R.ok(loginDto,"");
}
@@ -150,4 +169,22 @@ public class SysLoginController
loginService.resetPassword(loginBody.getUsername(),loginBody.getPassword(),loginBody.getRepeatPassword(),loginBody.getCode(),loginBody.getUuid());
return AjaxResult.success(MessageUtils.messageCustomize("systemCommonTip10001"));
}
/**
* 退出登录
*
* @return 结果
*/
@ApiOperation("退出登录")
@PostMapping("/logout")
public AjaxResult logout()
{
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser != null)
{
// 删除用户缓存记录
tokenService.delLoginUser(loginUser.getToken());
}
return AjaxResult.success("退出成功");
}
}

View File

@@ -0,0 +1,449 @@
package com.vetti.web.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.vetti.common.config.HotakeOAuth2Properties;
import com.vetti.common.core.domain.entity.SysUser;
import com.vetti.common.core.domain.model.LoginUser;
import com.vetti.common.enums.UserFlagEnum;
import com.vetti.common.exception.ServiceException;
import com.vetti.common.utils.DateUtils;
import com.vetti.common.utils.ip.AddressUtils;
import com.vetti.common.utils.ip.IpUtils;
import com.vetti.framework.web.service.SysPermissionService;
import com.vetti.framework.web.service.TokenService;
import com.vetti.hotake.domain.HotakeCvInfo;
import com.vetti.hotake.domain.HotakeSocialLoginLog;
import com.vetti.hotake.domain.HotakeSocialUser;
import com.vetti.hotake.domain.dto.HotakeSocialLoginRequestDto;
import com.vetti.hotake.domain.dto.HotakeSocialLoginResultDto;
import com.vetti.hotake.domain.dto.HotakeSocialUserInfoDto;
import com.vetti.hotake.mapper.HotakeCvInfoMapper;
import com.vetti.hotake.mapper.HotakeSocialLoginLogMapper;
import com.vetti.hotake.mapper.HotakeSocialUserMapper;
import com.vetti.hotake.service.IHotakeSocialUserService;
import com.vetti.system.service.ISysUserService;
import eu.bitwalker.useragentutils.UserAgent;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 社交用户服务实现
*
* @author vetti
*/
@Service
public class HotakeSocialUserServiceImpl implements IHotakeSocialUserService {
private static final Logger log = LoggerFactory.getLogger(HotakeSocialUserServiceImpl.class);
@Autowired
private HotakeOAuth2Properties oAuth2Properties;
@Autowired
private HotakeSocialUserMapper socialUserMapper;
@Autowired
private HotakeSocialLoginLogMapper socialLoginLogMapper;
@Autowired
private ISysUserService sysUserService;
@Autowired
private TokenService tokenService;
@Autowired
private SysPermissionService permissionService;
@Autowired
private HotakeCvInfoMapper cvInfoMapper;
private final OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
@Override
@Transactional(rollbackFor = Exception.class)
public HotakeSocialLoginResultDto socialLogin(HotakeSocialLoginRequestDto requestDto) {
String provider = requestDto.getProvider().toLowerCase();
String code = requestDto.getCode();
// 1. 获取第三方用户信息
HotakeSocialUserInfoDto socialUserInfo = getSocialUserInfo(provider, code);
if (socialUserInfo == null || StrUtil.isBlank(socialUserInfo.getProviderUserId())) {
recordLoginLog(null, provider, null, "login", "1", "获取第三方用户信息失败");
throw new ServiceException("获取第三方用户信息失败");
}
// 2. 查询是否已绑定
HotakeSocialUser existSocialUser = socialUserMapper.selectByProviderAndProviderUserId(
provider, socialUserInfo.getProviderUserId());
SysUser sysUser;
String loginType;
boolean isNewUser = false;
if (existSocialUser != null && existSocialUser.getUserId() != null) {
// 已绑定,直接登录
loginType = "login";
sysUser = sysUserService.selectUserById(existSocialUser.getUserId());
if (sysUser == null) {
recordLoginLog(null, provider, socialUserInfo.getProviderUserId(), loginType, "1", "绑定的用户不存在");
throw new ServiceException("绑定的用户不存在");
}
// 更新token信息
updateSocialUserToken(existSocialUser, socialUserInfo);
} else {
// 未绑定,自动注册
loginType = "register";
isNewUser = true;
sysUser = autoRegister(socialUserInfo, requestDto.getSysUserType());
// 如果是已存在用户绑定,则不是新用户
if (sysUser.getCreateTime() != null &&
System.currentTimeMillis() - sysUser.getCreateTime().getTime() > 5000) {
isNewUser = false;
}
// 创建社交绑定
createSocialUserBinding(sysUser.getUserId(), socialUserInfo);
}
// 3. 生成登录Token
HotakeSocialLoginResultDto resultDto = createLoginToken(sysUser, provider, isNewUser);
// 4. 记录登录日志
recordLoginLog(sysUser.getUserId(), provider, socialUserInfo.getProviderUserId(), loginType, "0", "登录成功");
// 5. 更新登录信息
recordLoginInfo(sysUser.getUserId());
return resultDto;
}
@Override
public String getAuthorizationUrl(String provider, String state) {
HotakeOAuth2Properties.OAuthClientConfig config = oAuth2Properties.getByProvider(provider);
if (config == null) {
throw new ServiceException("不支持的登录平台: " + provider);
}
try {
StringBuilder url = new StringBuilder(config.getAuthUri());
url.append("?client_id=").append(URLEncoder.encode(config.getClientId(), StandardCharsets.UTF_8.name()));
url.append("&redirect_uri=").append(URLEncoder.encode(config.getRedirectUri(), StandardCharsets.UTF_8.name()));
url.append("&response_type=code");
url.append("&scope=").append(URLEncoder.encode(config.getScope(), StandardCharsets.UTF_8.name()));
if (StrUtil.isNotBlank(state)) {
url.append("&state=").append(URLEncoder.encode(state, StandardCharsets.UTF_8.name()));
}
return url.toString();
} catch (Exception e) {
throw new ServiceException("生成授权URL失败");
}
}
@Override
public List<HotakeSocialUser> listByUserId(Long userId) {
return socialUserMapper.selectByUserId(userId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void unbind(Long userId, String provider) {
HotakeSocialUser socialUser = socialUserMapper.selectByUserIdAndProvider(userId, provider);
if (socialUser == null) {
throw new ServiceException("未绑定该平台账号");
}
socialUserMapper.deleteById(socialUser.getId());
}
/**
* 获取第三方用户信息
*/
private HotakeSocialUserInfoDto getSocialUserInfo(String provider, String code) {
HotakeOAuth2Properties.OAuthClientConfig config = oAuth2Properties.getByProvider(provider);
if (config == null) {
throw new ServiceException("不支持的登录平台: " + provider);
}
// 1. 用code换取access_token
JSONObject tokenResponse = getAccessToken(config, code);
if (tokenResponse == null) {
return null;
}
String accessToken = tokenResponse.getString("access_token");
String refreshToken = tokenResponse.getString("refresh_token");
Long expiresIn = tokenResponse.getLong("expires_in");
// 2. 用access_token获取用户信息
JSONObject userInfo = getUserInfo(config, accessToken);
if (userInfo == null) {
return null;
}
// 3. 解析用户信息(不同平台字段不同)
return parseSocialUserInfo(provider, userInfo, accessToken, refreshToken, expiresIn);
}
/**
* 获取access_token
*/
private JSONObject getAccessToken(HotakeOAuth2Properties.OAuthClientConfig config, String code) {
try {
FormBody.Builder formBuilder = new FormBody.Builder()
.add("client_id", config.getClientId())
.add("client_secret", config.getClientSecret())
.add("code", code)
.add("redirect_uri", config.getRedirectUri())
.add("grant_type", "authorization_code");
Request request = new Request.Builder()
.url(config.getTokenUri())
.post(formBuilder.build())
.addHeader("Accept", "application/json")
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String body = response.body().string();
log.debug("Token response: {}", body);
return JSON.parseObject(body);
}
}
} catch (IOException e) {
log.error("获取access_token失败", e);
}
return null;
}
/**
* 获取用户信息
*/
private JSONObject getUserInfo(HotakeOAuth2Properties.OAuthClientConfig config, String accessToken) {
try {
Request request = new Request.Builder()
.url(config.getUserInfoUri())
.get()
.addHeader("Authorization", "Bearer " + accessToken)
.addHeader("Accept", "application/json")
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.isSuccessful() && response.body() != null) {
String body = response.body().string();
log.debug("UserInfo response: {}", body);
return JSON.parseObject(body);
}
}
} catch (IOException e) {
log.error("获取用户信息失败", e);
}
return null;
}
/**
* 解析不同平台的用户信息为统一格式
*/
private HotakeSocialUserInfoDto parseSocialUserInfo(String provider, JSONObject userInfo,
String accessToken, String refreshToken, Long expiresIn) {
HotakeSocialUserInfoDto dto = new HotakeSocialUserInfoDto();
dto.setProvider(provider);
dto.setAccessToken(accessToken);
dto.setRefreshToken(refreshToken);
dto.setExpiresIn(expiresIn);
dto.setRawUserInfo(userInfo.toJSONString());
switch (provider.toLowerCase()) {
case "google":
dto.setProviderUserId(userInfo.getString("sub"));
dto.setEmail(userInfo.getString("email"));
dto.setName(userInfo.getString("name"));
dto.setAvatar(userInfo.getString("picture"));
break;
case "microsoft":
dto.setProviderUserId(userInfo.getString("id"));
// Microsoft返回的邮箱字段是mail或userPrincipalName
String email = userInfo.getString("mail");
if (StrUtil.isBlank(email)) {
email = userInfo.getString("userPrincipalName");
}
dto.setEmail(email);
dto.setName(userInfo.getString("displayName"));
// Microsoft头像需要单独请求这里暂不处理
break;
case "linkedin":
dto.setProviderUserId(userInfo.getString("sub"));
dto.setEmail(userInfo.getString("email"));
dto.setName(userInfo.getString("name"));
dto.setAvatar(userInfo.getString("picture"));
break;
default:
throw new ServiceException("不支持的登录平台: " + provider);
}
return dto;
}
/**
* 自动注册用户
*/
private SysUser autoRegister(HotakeSocialUserInfoDto socialUserInfo, String sysUserType) {
// 检查邮箱是否已存在
if (StrUtil.isNotBlank(socialUserInfo.getEmail())) {
SysUser existUser = sysUserService.selectUserByUserName(socialUserInfo.getEmail());
if (existUser != null) {
// 邮箱已存在,直接绑定
return existUser;
}
}
// 创建新用户
SysUser sysUser = new SysUser();
sysUser.setUserName(socialUserInfo.getEmail());
sysUser.setEmail(socialUserInfo.getEmail());
sysUser.setNickName(socialUserInfo.getName());
sysUser.setAvatar(socialUserInfo.getAvatar());
sysUser.setSysUserType(StrUtil.isNotBlank(sysUserType) ? sysUserType : "candidate");
sysUser.setUserFlag(UserFlagEnum.FLAG_1.getCode());
sysUser.setUserOperStatus("1");
sysUser.setPwdUpdateDate(DateUtils.getNowDate());
// 社交登录用户不设置密码,或设置随机密码
sysUser.setPassword("");
boolean success = sysUserService.registerUser(sysUser);
if (!success) {
throw new ServiceException("自动注册用户失败");
}
return sysUser;
}
/**
* 创建社交账号绑定
*/
private void createSocialUserBinding(Long userId, HotakeSocialUserInfoDto socialUserInfo) {
HotakeSocialUser socialUser = new HotakeSocialUser();
socialUser.setUserId(userId);
socialUser.setProvider(socialUserInfo.getProvider());
socialUser.setProviderUserId(socialUserInfo.getProviderUserId());
socialUser.setEmail(socialUserInfo.getEmail());
socialUser.setName(socialUserInfo.getName());
socialUser.setAvatar(socialUserInfo.getAvatar());
socialUser.setAccessToken(socialUserInfo.getAccessToken());
socialUser.setRefreshToken(socialUserInfo.getRefreshToken());
if (socialUserInfo.getExpiresIn() != null) {
socialUser.setTokenExpireTime(new Date(System.currentTimeMillis() + socialUserInfo.getExpiresIn() * 1000));
}
socialUser.setRawUserInfo(socialUserInfo.getRawUserInfo());
socialUserMapper.insert(socialUser);
}
/**
* 更新社交账号token信息
*/
private void updateSocialUserToken(HotakeSocialUser socialUser, HotakeSocialUserInfoDto socialUserInfo) {
socialUser.setAccessToken(socialUserInfo.getAccessToken());
socialUser.setRefreshToken(socialUserInfo.getRefreshToken());
if (socialUserInfo.getExpiresIn() != null) {
socialUser.setTokenExpireTime(new Date(System.currentTimeMillis() + socialUserInfo.getExpiresIn() * 1000));
}
socialUser.setRawUserInfo(socialUserInfo.getRawUserInfo());
socialUserMapper.update(socialUser);
}
/**
* 创建登录Token
*/
private HotakeSocialLoginResultDto createLoginToken(SysUser sysUser, String provider, boolean isNewUser) {
// 获取权限
Set<String> permissions = permissionService.getMenuPermission(sysUser);
// 创建LoginUser
LoginUser loginUser = new LoginUser(sysUser.getUserId(), sysUser.getDeptId(), sysUser, permissions);
// 生成token
String token = tokenService.createToken(loginUser);
// 构建返回对象
HotakeSocialLoginResultDto resultDto = new HotakeSocialLoginResultDto();
resultDto.setToken(token);
resultDto.setUserId(sysUser.getUserId());
resultDto.setSysUserType(sysUser.getSysUserType());
resultDto.setIsNewUser(isNewUser);
resultDto.setProvider(provider);
resultDto.setUser(sysUser);
// 如果是候选者,查询是否有简历
if ("candidate".equals(sysUser.getSysUserType())) {
HotakeCvInfo query = new HotakeCvInfo();
query.setUserId(sysUser.getUserId());
query.setCvFileType("cv"); // 只查询简历类型的文件
List<HotakeCvInfo> cvList = cvInfoMapper.selectHotakeCvInfoList(query);
// 设置简历状态标识
resultDto.setHasCv(!CollectionUtil.isEmpty(cvList));
resultDto.setCvCount(cvList != null ? cvList.size() : 0);
}
return resultDto;
}
/**
* 记录登录信息
*/
private void recordLoginInfo(Long userId) {
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setLoginIp(IpUtils.getIpAddr());
sysUser.setLoginDate(DateUtils.getNowDate());
sysUserService.updateUserProfile(sysUser);
}
/**
* 记录社交登录日志
*/
private void recordLoginLog(Long userId, String provider, String providerUserId,
String loginType, String status, String msg) {
try {
HotakeSocialLoginLog loginLog = new HotakeSocialLoginLog();
loginLog.setUserId(userId);
loginLog.setProvider(provider);
loginLog.setProviderUserId(providerUserId);
loginLog.setLoginType(loginType);
loginLog.setLoginIp(IpUtils.getIpAddr());
loginLog.setLoginLocation(AddressUtils.getRealAddressByIP(IpUtils.getIpAddr()));
try {
UserAgent userAgent = UserAgent.parseUserAgentString(
com.vetti.common.utils.ServletUtils.getRequest().getHeader("User-Agent"));
loginLog.setBrowser(userAgent.getBrowser().getName());
loginLog.setOs(userAgent.getOperatingSystem().getName());
} catch (Exception ignored) {
}
loginLog.setStatus(status);
loginLog.setMsg(msg);
loginLog.setLoginTime(new Date());
socialLoginLogMapper.insert(loginLog);
} catch (Exception e) {
log.error("记录社交登录日志失败", e);
}
}
}

View File

@@ -171,3 +171,44 @@ chatGpt:
http:
client:
connect-timeout-seconds: 10
# OAuth2.0 社交登录配置
oauth2:
# ================================
# Google (Gmail) 登录配置
# 申请地址: https://console.cloud.google.com/apis/credentials
# ================================
google:
client-id: your-google-client-id # Google Cloud Console获取
client-secret: your-google-client-secret # Google Cloud Console获取
redirect-uri: https://your-domain.com/api/oauth2/callback/google
scope: openid email profile # Google标准scope空格分隔
auth-uri: https://accounts.google.com/o/oauth2/v2/auth # Google授权地址
token-uri: https://oauth2.googleapis.com/token # Google令牌地址
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo # Google用户信息接口
# ================================
# Microsoft (Outlook) 登录配置
# 申请地址: https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps
# ================================
microsoft:
client-id: your-microsoft-client-id # Azure Portal获取
client-secret: your-microsoft-client-secret # Azure Portal获取
redirect-uri: https://your-domain.com/api/oauth2/callback/microsoft
scope: openid,email,profile,User.Read # 需要额外的User.Read权限才能读取用户信息
auth-uri: https://login.microsoftonline.com/common/oauth2/v2.0/authorize # 微软授权地址
token-uri: https://login.microsoftonline.com/common/oauth2/v2.0/token # 微软令牌地址
user-info-uri: https://graph.microsoft.com/v1.0/me # 使用Microsoft Graph API
# ================================
# LinkedIn 登录配置
# 申请地址: https://www.linkedin.com/developers/apps
# ================================
linkedin:
client-id: 86uq3opzshd3bq # LinkedIn Developer Portal获取
client-secret: 86uq3opzshd3bq # LinkedIn Developer Portal获取
redirect-uri: http://localhost:8080/oauth2/callback/linkedin
scope: openid profile email # LinkedIn使用OpenID Connectscope顺序和命名略有不同
auth-uri: https://www.linkedin.com/oauth/v2/authorization # LinkedIn授权地址
token-uri: https://www.linkedin.com/oauth/v2/accessToken # LinkedIn令牌地址
user-info-uri: https://api.linkedin.com/v2/userinfo # LinkedIn用户信息接口

View File

@@ -197,3 +197,44 @@ chatGpt:
http:
client:
connect-timeout-seconds: 600
# OAuth2.0 社交登录配置
oauth2:
# ================================
# Google (Gmail) 登录配置
# 申请地址: https://console.cloud.google.com/apis/credentials
# ================================
google:
client-id: 398978985110-ve0usu381mmdio12ff01iqvv1g087qvi.apps.googleusercontent.com # Google Cloud Console获取
client-secret: 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 # Google Cloud Console获取
redirect-uri: https://vetti.hotake.cn/oauth2/callback/google
scope: openid email profile # Google标准scope空格分隔
auth-uri: https://accounts.google.com/o/oauth2/v2/auth # Google授权地址
token-uri: https://oauth2.googleapis.com/token # Google令牌地址
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo # Google用户信息接口
# ================================
# Microsoft (Outlook) 登录配置
# 申请地址: https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps
# ================================
microsoft:
client-id: 608cbc4f-3ee2-4f51-a72c-b2e3133fc5b2 # Azure Portal获取
client-secret: gvw8Q~dwG8Sv7HN3R3W3R7TQtcZyvrh88ZiJubPa # Azure Portal获取
redirect-uri: https://vetti.hotake.cn/api/oauth2/callback/microsoft
scope: openid,email,profile,User.Read # 需要额外的User.Read权限才能读取用户信息
auth-uri: https://login.microsoftonline.com/common/oauth2/v2.0/authorize # 微软授权地址
token-uri: https://login.microsoftonline.com/common/oauth2/v2.0/token # 微软令牌地址
user-info-uri: https://graph.microsoft.com/v1.0/me # 使用Microsoft Graph API
# ================================
# LinkedIn 登录配置
# 申请地址: https://www.linkedin.com/developers/apps
# ================================
linkedin:
client-id: 86uq3opzshd3bq # LinkedIn Developer Portal获取
client-secret: WPL_AP1.mipgyxfgfBoN12Th.1TXeFg== # LinkedIn Developer Portal获取
redirect-uri: https://vetti.hotake.cn/api/oauth2/callback/linkedin
scope: openid profile email # LinkedIn使用OpenID Connectscope顺序和命名略有不同
auth-uri: https://www.linkedin.com/oauth/v2/authorization # LinkedIn授权地址
token-uri: https://www.linkedin.com/oauth/v2/accessToken # LinkedIn令牌地址
user-info-uri: https://api.linkedin.com/v2/userinfo # LinkedIn用户信息接口

View File

@@ -63,3 +63,6 @@ HotakeRolesInfoServiceImpl10001 = The job information is abnormal. Please try ag
# manager.页面,字段 = User Manager
VerificationEmailTiTle = Your verification code
VerificationEmailContent = Your verification code is: {0}, valid for {1} minutes.
HotakeRolesApplyInfoServiceImpl10001 = You have already applied for this position

View File

@@ -62,3 +62,4 @@ HotakeRolesInfoServiceImpl10001 = 岗位信息异常,请稍后再试
VerificationEmailTiTle = 你的验证码
VerificationEmailContent = 你的验证码是: {0},有效期为 {1} 分钟。
HotakeRolesApplyInfoServiceImpl10001 = 您已申请该职位

View File

@@ -197,3 +197,44 @@ chatGpt:
http:
client:
connect-timeout-seconds: 600
# OAuth2.0 社交登录配置
oauth2:
# ================================
# Google (Gmail) 登录配置
# 申请地址: https://console.cloud.google.com/apis/credentials
# ================================
google:
client-id: 398978985110-ve0usu381mmdio12ff01iqvv1g087qvi.apps.googleusercontent.com # Google Cloud Console获取
client-secret: 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 # Google Cloud Console获取
redirect-uri: https://vetti.hotake.cn/oauth2/callback/google
scope: openid email profile # Google标准scope空格分隔
auth-uri: https://accounts.google.com/o/oauth2/v2/auth # Google授权地址
token-uri: https://oauth2.googleapis.com/token # Google令牌地址
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo # Google用户信息接口
# ================================
# Microsoft (Outlook) 登录配置
# 申请地址: https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps
# ================================
microsoft:
client-id: 608cbc4f-3ee2-4f51-a72c-b2e3133fc5b2 # Azure Portal获取
client-secret: gvw8Q~dwG8Sv7HN3R3W3R7TQtcZyvrh88ZiJubPa # Azure Portal获取
redirect-uri: https://vetti.hotake.cn/api/oauth2/callback/microsoft
scope: openid,email,profile,User.Read # 需要额外的User.Read权限才能读取用户信息
auth-uri: https://login.microsoftonline.com/common/oauth2/v2.0/authorize # 微软授权地址
token-uri: https://login.microsoftonline.com/common/oauth2/v2.0/token # 微软令牌地址
user-info-uri: https://graph.microsoft.com/v1.0/me # 使用Microsoft Graph API
# ================================
# LinkedIn 登录配置
# 申请地址: https://www.linkedin.com/developers/apps
# ================================
linkedin:
client-id: 86uq3opzshd3bq # LinkedIn Developer Portal获取
client-secret: WPL_AP1.mipgyxfgfBoN12Th.1TXeFg== # LinkedIn Developer Portal获取
redirect-uri: https://vetti.hotake.cn/api/oauth2/callback/linkedin
scope: openid profile email # LinkedIn使用OpenID Connectscope顺序和命名略有不同
auth-uri: https://www.linkedin.com/oauth/v2/authorization # LinkedIn授权地址
token-uri: https://www.linkedin.com/oauth/v2/accessToken # LinkedIn令牌地址
user-info-uri: https://api.linkedin.com/v2/userinfo # LinkedIn用户信息接口

View File

@@ -63,3 +63,6 @@ HotakeRolesInfoServiceImpl10001 = The job information is abnormal. Please try ag
# manager.页面,字段 = User Manager
VerificationEmailTiTle = Your verification code
VerificationEmailContent = Your verification code is: {0}, valid for {1} minutes.
HotakeRolesApplyInfoServiceImpl10001 = You have already applied for this position

View File

@@ -62,3 +62,4 @@ HotakeRolesInfoServiceImpl10001 = 岗位信息异常,请稍后再试
VerificationEmailTiTle = 你的验证码
VerificationEmailContent = 你的验证码是: {0},有效期为 {1} 分钟。
HotakeRolesApplyInfoServiceImpl10001 = 您已申请该职位