| | |
| | | package com.ruoyi.im.service.impl; |
| | | |
| | | import cn.hutool.core.util.ObjectUtil; |
| | | import cn.hutool.json.JSONUtil; |
| | | import com.alibaba.fastjson2.JSON; |
| | | import com.alibaba.fastjson2.JSONObject; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.fasterxml.jackson.core.JsonProcessingException; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import com.netease.nim.server.sdk.core.BizName; |
| | | import com.netease.nim.server.sdk.core.YunxinApiHttpClient; |
| | | import com.netease.nim.server.sdk.core.YunxinApiResponse; |
| | | import com.netease.nim.server.sdk.core.exception.YunxinSdkException; |
| | | import com.netease.nim.server.sdk.core.http.HttpMethod; |
| | | import com.netease.nim.server.sdk.im.v2.friend.FriendV2UrlContext; |
| | | import com.netease.nim.server.sdk.im.v2.team.TeamV2UrlContext; |
| | | import com.ruoyi.common.core.domain.AjaxResult; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.im.comm.Result; |
| | | import com.ruoyi.im.config.AppAuthConfig; |
| | | import com.ruoyi.im.config.*; |
| | | import com.ruoyi.im.dto.UpdateUserBusinessDto; |
| | | import com.ruoyi.im.service.NeteaseTeamService; |
| | | import com.ruoyi.imenum.ErrorCodeEnum; |
| | | import com.ruoyi.system.domain.GroupWelcomeConfig; |
| | | import com.ruoyi.system.domain.InvitationBlacklist; |
| | | import com.ruoyi.system.domain.NeteaseTeam; |
| | | import com.ruoyi.system.domain.UserAccount; |
| | | import com.ruoyi.im.service.ImApiServcie; |
| | | import com.ruoyi.im.dto.RegisterDto; |
| | | import com.ruoyi.system.domain.vo.UserAccountUpdateVo; |
| | | import com.ruoyi.system.mapper.NeteaseTeamMapper; |
| | | import com.ruoyi.system.service.GroupWelcomeConfigService; |
| | | import com.ruoyi.system.service.UserAccountService; |
| | | import com.ruoyi.im.util.SymmetricCryptoUtil; |
| | | import com.ruoyi.system.service.impl.InvitationBlacklistServiceImpl; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.codec.digest.DigestUtils; |
| | | import org.apache.http.HttpResponse; |
| | | import org.apache.http.client.methods.HttpPatch; |
| | | import org.apache.http.entity.StringEntity; |
| | | import org.apache.http.impl.client.CloseableHttpClient; |
| | | import org.apache.http.impl.client.HttpClients; |
| | | import org.apache.http.util.EntityUtils; |
| | | import org.apache.poi.util.StringUtil; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.jdbc.core.BatchPreparedStatementSetter; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.transaction.interceptor.TransactionAspectSupport; |
| | | import org.springframework.util.CollectionUtils; |
| | | |
| | | import javax.annotation.PostConstruct; |
| | | import javax.annotation.Resource; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.sql.PreparedStatement; |
| | | import java.sql.SQLException; |
| | | import java.time.Instant; |
| | | import java.util.*; |
| | | import java.util.concurrent.CompletableFuture; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.ThreadLocalRandom; |
| | | import java.util.concurrent.atomic.AtomicLong; |
| | | import java.util.stream.Collectors; |
| | | |
| | | @Service |
| | | @Slf4j |
| | | public class ImApiServcieImpl implements ImApiServcie { |
| | | @Autowired |
| | | private UserAccountService userAccountService; |
| | | @Autowired |
| | | GroupWelcomeConfigService groupWelcomeConfigService; |
| | | @Autowired |
| | | NeteaseTeamService neteaseTeamService; |
| | | |
| | | // 使用单例客户端,避免重复初始化 |
| | | private YunxinApiHttpClient yunxinClient; |
| | | @Autowired |
| | | private JdbcTemplate jdbcTemplate; |
| | | @Autowired |
| | | private NeteaseTeamMapper neteaseTeamMapper; |
| | | |
| | | @Autowired |
| | | InvitationBlacklistServiceImpl invitationBlacklistService; |
| | | |
| | | @Resource |
| | | private final YunxinApiHttpClient yunxinClient; |
| | | |
| | | // 使用构造函数注入(推荐) |
| | | @Autowired |
| | | public ImApiServcieImpl(YunxinApiHttpClient yunxinClient) { |
| | | this.yunxinClient = yunxinClient; |
| | | } |
| | | |
| | | |
| | | // 使用并发安全的集合存储正在生成的账号,防止重复 |
| | | private final Set<Long> generatingAccounts = Collections.newSetFromMap(new ConcurrentHashMap<>()); |
| | |
| | | private final AtomicLong lastTimestamp = new AtomicLong(0); |
| | | private final AtomicLong sequence = new AtomicLong(0); |
| | | |
| | | @PostConstruct |
| | | public void init() { |
| | | // 初始化云信客户端,只执行一次 |
| | | String appkey = AppAuthConfig.DEFAULT_CONFIG.getAppKey(); |
| | | String appsecret = AppAuthConfig.DEFAULT_CONFIG.getAppSecret(); |
| | | int timeoutMillis = 5000; |
| | | private static final String DEFAULT_PASSWORD = "123456";//批量注册密码 |
| | | private static final String ENCRYPTED_PASSWORD = SymmetricCryptoUtil.encryptPassword(DEFAULT_PASSWORD); // 密码加密一次,多次使用 |
| | | |
| | | this.yunxinClient = new YunxinApiHttpClient.Builder(BizName.IM, appkey, appsecret) |
| | | .timeoutMillis(timeoutMillis) |
| | | .build(); |
| | | } |
| | | private static final String YUNXIN_CREATE_PATH = "/user/create.action"; |
| | | private static final String FRIENDS_PATH = "/im/v2.1/friends"; |
| | | |
| | | @Value("${netease.im.api-head-portrait-url}") |
| | | private String headPortraitUrl; |
| | | |
| | | private final ObjectMapper objectMapper = new ObjectMapper(); |
| | | |
| | | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Result register(RegisterDto dto) { |
| | | // 短信验证 TODO |
| | | |
| | | // 手机号是否存在 - 添加分布式锁或数据库唯一索引确保并发安全 |
| | | // 验证手机号是否已存在 |
| | | List<UserAccount> accounts = userAccountService.list( |
| | | new LambdaQueryWrapper<>(UserAccount.class) |
| | | .eq(UserAccount::getAccount, dto.getAccount()) |
| | | ); |
| | | |
| | | if(!CollectionUtils.isEmpty(accounts)){ |
| | | return Result.error("手机号已注册!"); |
| | | } |
| | | |
| | | // // 生成云信账号 |
| | | // long cloudMessageAccount; |
| | | // try { |
| | | // cloudMessageAccount = generateUniqueCloudMessageAccount(); |
| | | // } catch (RuntimeException e) { |
| | | // return Result.error("系统繁忙,请稍后重试"); |
| | | // } |
| | | |
| | | // 注册云信 |
| | | String path = "/user/create.action"; |
| | | Map<String, String> paramMap = new HashMap<>(); |
| | | paramMap.put("accid", String.valueOf(dto.getAccount())); |
| | | paramMap.put("token", dto.getPassword()); |
| | | |
| | | YunxinApiResponse response; |
| | | try { |
| | | response = yunxinClient.executeV1Api(path, paramMap); |
| | | } catch (YunxinSdkException e) { |
| | | System.err.println("register error, traceId = " + e.getTraceId()); |
| | | return Result.error("注册失败,系统异常"); |
| | | } |
| | | |
| | | // 获取结果 |
| | | String data = response.getData(); |
| | | JSONObject json = JSONObject.parseObject(data); |
| | | int code = json.getIntValue("code"); |
| | | |
| | | if (code == 200) { |
| | | // 注册成功 |
| | | UserAccount userAccount = new UserAccount(); |
| | | userAccount.setAccount(dto.getAccount()); |
| | | userAccount.setPhoneNumber(dto.getAccount()); |
| | | userAccount.setCloudMessageAccount(dto.getAccount()); |
| | | userAccount.setPassword(SymmetricCryptoUtil.encryptPassword(dto.getPassword())); |
| | | |
| | | try { |
| | | userAccountService.save(userAccount); |
| | | return Result.success("注册成功"); |
| | | } catch (Exception e) { |
| | | // 处理数据库插入异常,可能需要回滚云信账号 |
| | | return Result.error("注册失败,请重试"); |
| | | } |
| | | } else if (code == 414) { |
| | | if (!CollectionUtils.isEmpty(accounts)) { |
| | | return Result.error("账号已被注册!"); |
| | | } else { |
| | | System.err.println("register fail, response = " + data + ", traceId=" + response.getTraceId()); |
| | | return Result.error("注册失败,错误码: " + code); |
| | | } |
| | | |
| | | if(dto.getAccountType() == 0 && StringUtils.isEmpty(dto.getInvitationCode())){ |
| | | return Result.error("邀请码不能为空!"); |
| | | } |
| | | |
| | | long count = invitationBlacklistService.count(new LambdaQueryWrapper<InvitationBlacklist>() |
| | | .eq(InvitationBlacklist::getInvitationCode, dto.getInvitationCode()) |
| | | ); |
| | | if(count > 0){ |
| | | return Result.error("邀请码已被限制邀请!"); |
| | | } |
| | | |
| | | String invitationCode = getInvitationCode(); |
| | | UserAccount user = new UserAccount(); |
| | | if(dto.getAccountType() == 0 && StringUtils.isNotEmpty(dto.getInvitationCode()) && !dto.getInvitationCode().equals("00000000")){ |
| | | user = userAccountService.getOne(new LambdaQueryWrapper<UserAccount>() |
| | | .eq(UserAccount::getInvitationCode, dto.getInvitationCode()).last(" limit 1")); |
| | | if(ObjectUtil.isEmpty(user)){ |
| | | return Result.error("邀请码错误"); |
| | | } |
| | | } |
| | | |
| | | // 创建本地用户账户记录 |
| | | UserAccount userAccount = new UserAccount(); |
| | | userAccount.setAccount(dto.getAccount()); |
| | | userAccount.setPhoneNumber(dto.getAccount()); |
| | | userAccount.setCloudMessageAccount(dto.getAccount()); |
| | | userAccount.setPassword(SymmetricCryptoUtil.encryptPassword(dto.getPassword())); |
| | | userAccount.setCreateTime(new Date()); |
| | | userAccount.setNickname(dto.getNikeName()); |
| | | userAccount.setCreateTime(new Date()); |
| | | userAccount.setUpdateTime(new Date()); |
| | | userAccount.setInvitationCode(invitationCode); |
| | | userAccount.setInvitationAccount(ObjectUtil.isNotEmpty(user.getAccount()) ? user.getAccount() : ""); |
| | | |
| | | if (!userAccountService.save(userAccount)) { |
| | | throw new RuntimeException("保存用户账户失败"); |
| | | } |
| | | try { |
| | | // 注册云信账号(远程调用) |
| | | Map<String, String> paramMap = new HashMap<>(); |
| | | paramMap.put("accid", dto.getAccount()); |
| | | paramMap.put("token", dto.getPassword()); |
| | | |
| | | if (StringUtils.isNotEmpty(dto.getNikeName())) { |
| | | Map<String, String> userInfoMap = new HashMap<>(); |
| | | userInfoMap.put("name", dto.getNikeName()); |
| | | |
| | | // 使用 JSON 工具将 Map 转为 JSON 字符串 |
| | | ObjectMapper objectMapper = new ObjectMapper(); |
| | | String userInfoJson = objectMapper.writeValueAsString(userInfoMap); |
| | | paramMap.put("user_information", userInfoJson); |
| | | } |
| | | YunxinApiResponse response = yunxinClient.executeV1Api(YUNXIN_CREATE_PATH, paramMap); |
| | | |
| | | // 处理云信响应 |
| | | String data = response.getData(); |
| | | JSONObject json = JSONObject.parseObject(data); |
| | | int code = json.getIntValue("code"); |
| | | |
| | | if (code != 200) { |
| | | String errorMsg = ""; |
| | | if(code == 102405){ |
| | | errorMsg = "用户已存在"; |
| | | } |
| | | log.error("-----------注册账号异常:"+ErrorCodeEnum.getByCode(code).getComment()+"----im信息:"+ErrorCodeEnum.getByCode(code).getDesc()); |
| | | throw new RuntimeException(errorMsg); |
| | | } |
| | | //修改昵称 |
| | | UpdateUserBusinessDto userBusinessDto = new UpdateUserBusinessDto(); |
| | | userBusinessDto.setName(dto.getNikeName()); |
| | | updateUserAvatar(dto.getAccount(),userBusinessDto); |
| | | |
| | | //默认添加邀请人为好友 |
| | | if(ObjectUtil.isNotEmpty(user)){ |
| | | addFriends(userAccount.getAccount(),user.getAccount()); |
| | | } |
| | | |
| | | // 注册成功后的其他操作 |
| | | GroupWelcomeConfig groupWelcomeConfig = groupWelcomeConfigService.getOne(new LambdaQueryWrapper<>(GroupWelcomeConfig.class) |
| | | .eq(GroupWelcomeConfig::getConfigurationName, "IM-BASICS").last(" limit 1")); |
| | | |
| | | NeteaseTeam neteaseTeam = neteaseTeamMapper.selectOne(new LambdaQueryWrapper<NeteaseTeam>().eq(NeteaseTeam::getTid,groupWelcomeConfig.getGroupId())); |
| | | if(ObjectUtil.isNotEmpty(groupWelcomeConfig) && ObjectUtil.isNotEmpty(groupWelcomeConfig.getUserAccid())){ |
| | | addFriends(userAccount.getAccount(),groupWelcomeConfig.getUserAccid()); |
| | | } |
| | | if(ObjectUtil.isNotEmpty(groupWelcomeConfig) && ObjectUtil.isNotEmpty(groupWelcomeConfig.getGroupId()) && ObjectUtil.isNotEmpty(neteaseTeam)){ |
| | | List<String> accountList = new ArrayList<>(); |
| | | accountList.add(userAccount.getAccount()); |
| | | AddTeamMembersRequest request = new AddTeamMembersRequest(); |
| | | request.setInviteAccountIds(accountList); |
| | | request.setGroupId(ObjectUtil.isNotEmpty(neteaseTeam.getId().toString()) ? neteaseTeam.getId().toString() : null); |
| | | neteaseTeamService.inviteTeamMembers(request); |
| | | } |
| | | return Result.success("注册成功"); |
| | | |
| | | } catch (Exception e) { |
| | | log.error("注册过程发生异常", e); |
| | | // 将异常包装为Result并抛出RuntimeException触发回滚 |
| | | throw new RuntimeException(Result.error("注册失败: " + e.getMessage()).toString(), e); |
| | | } |
| | | } |
| | | |
| | | private String getInvitationCode() { |
| | | String invitationCode = null; |
| | | int maxAttempts = 100; // 最大尝试次数 |
| | | int attempts = 0; |
| | | |
| | | while (attempts < maxAttempts) { |
| | | invitationCode = generateInvitationCode(); |
| | | long count = userAccountService.count(new LambdaQueryWrapper<UserAccount>() |
| | | .eq(UserAccount::getInvitationCode, invitationCode)); |
| | | if(count <= 0){ |
| | | break; |
| | | } |
| | | attempts++; |
| | | } |
| | | |
| | | if (attempts >= maxAttempts) { |
| | | log.error("生成邀请码已超最大尝试次数!"); |
| | | throw new RuntimeException("无法生成唯一的邀请码,请稍后重试"); |
| | | } |
| | | return invitationCode; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 生成邀请码 |
| | | * @return |
| | | */ |
| | | public static String generateInvitationCode() { |
| | | Random random = new Random(); |
| | | int code = 10000000 + random.nextInt(90000000); |
| | | return String.valueOf(code); |
| | | } |
| | | |
| | | /** |
| | |
| | | try { |
| | | Thread.sleep(ThreadLocalRandom.current().nextInt(10, 50)); |
| | | } catch (InterruptedException e) { |
| | | e.printStackTrace(); |
| | | Thread.currentThread().interrupt(); |
| | | throw new RuntimeException("生成账号被中断", e); |
| | | } |
| | |
| | | // 组合时间戳和序列号,生成9位ID |
| | | return timestamp * 100 + seq; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * 更新用户名片 |
| | | * @param accountId 用户账号 |
| | | * @return 操作结果 |
| | | */ |
| | | public Map<String, Object> updateUserAvatar(String accountId, UpdateUserBusinessDto dto) { |
| | | Map<String, Object> result = new HashMap<>(); |
| | | |
| | | try (CloseableHttpClient httpClient = HttpClients.createDefault()) { |
| | | // 生成请求参数 |
| | | String nonce = UUID.randomUUID().toString().replace("-", ""); |
| | | String curTime = String.valueOf(System.currentTimeMillis() / 1000); |
| | | String checkSum = generateCheckSum(nonce, curTime); |
| | | |
| | | // 构建请求URL |
| | | String url = headPortraitUrl + "/im/v2/users/" + accountId; |
| | | |
| | | // 构建请求头 |
| | | HttpPatch httpPatch = new HttpPatch(url); |
| | | httpPatch.setHeader("Content-Type", "application/json;charset=utf-8"); |
| | | httpPatch.setHeader("AppKey", AppAuthConfig.DEFAULT_CONFIG.getAppKey()); |
| | | httpPatch.setHeader("Nonce", nonce); |
| | | httpPatch.setHeader("CurTime", curTime); |
| | | httpPatch.setHeader("CheckSum", checkSum); |
| | | |
| | | UpdateUserInfoRequest requestBody = new UpdateUserInfoRequest(dto.getAvatar(), |
| | | dto.getName(),dto.getSign(),dto.getEmail(),dto.getMobile(),dto.getGender()); |
| | | String jsonBody = objectMapper.writeValueAsString(requestBody); |
| | | httpPatch.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8)); |
| | | |
| | | // 执行请求 |
| | | HttpResponse response = httpClient.execute(httpPatch); |
| | | String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); |
| | | |
| | | // 解析响应 |
| | | NeteaseResponse neteaseResponse = objectMapper.readValue(responseString, NeteaseResponse.class); |
| | | |
| | | if (neteaseResponse.isSuccess()) { |
| | | result.put("success", true); |
| | | result.put("message", "更新成功"); |
| | | result.put("data", neteaseResponse.getData()); |
| | | } else { |
| | | result.put("success", false); |
| | | result.put("message", "更新失败: " + neteaseResponse.getMsg()); |
| | | result.put("errorCode", neteaseResponse.getCode()); |
| | | } |
| | | |
| | | } catch (Exception e) { |
| | | result.put("success", false); |
| | | result.put("message", "请求网易云信API失败: " + e.getMessage()); |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 生成校验和 |
| | | */ |
| | | private String generateCheckSum(String nonce, String curTime) { |
| | | String content = AppAuthConfig.DEFAULT_CONFIG.getAppSecret() + nonce + curTime; |
| | | return DigestUtils.sha1Hex(content); |
| | | } |
| | | |
| | | |
| | | |
| | | @Override |
| | | public AjaxResult updateUserAccount(UserAccountUpdateVo vo) { |
| | | //更新用户名片 |
| | | // UpdateUserBusinessDto dto = new UpdateUserBusinessDto(); |
| | | // if(StringUtils.isNotEmpty(vo.getPhoneNumber())){ |
| | | // dto.setMobile(vo.getPhoneNumber()); |
| | | // } |
| | | // if(StringUtils.isNotEmpty(vo.getNickname())){ |
| | | // dto.setName(vo.getNickname()); |
| | | // } |
| | | // if(StringUtils.isNotEmpty(vo.getSignature())){ |
| | | // dto.setSign(vo.getSignature()); |
| | | // } |
| | | // if(ObjectUtil.isNotEmpty(vo.getGender())){ |
| | | // dto.setGender(vo.getGender()); |
| | | // } |
| | | // Map<String, Object> map = updateUserAvatar(vo.getAccountId(), dto); |
| | | //更新用户属性 状态 密码 |
| | | // if ((Boolean) map.get("success")) { |
| | | AjaxResult ajaxResult = updateAccountProperties(vo.getAccount(), vo); |
| | | if(ajaxResult.isSuccess()){ |
| | | UserAccount userAccount = userAccountService.getOne(new LambdaQueryWrapper<UserAccount>() |
| | | .eq(UserAccount::getAccount,vo.getAccount()) |
| | | ); |
| | | if (StringUtils.isNotBlank(vo.getPhoneNumber())) { |
| | | userAccount.setPhoneNumber(vo.getPhoneNumber()); |
| | | } |
| | | |
| | | if (StringUtils.isNotBlank(vo.getAccount())) { |
| | | userAccount.setAccount(vo.getAccount()); |
| | | } |
| | | |
| | | if (StringUtils.isNotBlank(vo.getNickname())) { |
| | | userAccount.setNickname(vo.getNickname()); |
| | | } |
| | | |
| | | if (StringUtils.isNotBlank(vo.getPassword())) { |
| | | userAccount.setPassword(SymmetricCryptoUtil.encryptPassword(vo.getPassword())); |
| | | } |
| | | |
| | | if (StringUtils.isNotBlank(vo.getSignature())) { |
| | | userAccount.setSignature(vo.getSignature()); |
| | | } |
| | | userAccount.setGroupPermissions(vo.getGroupPermissions()); |
| | | userAccount.setAddFriend(vo.getAddFriend()); |
| | | userAccount.setStatus(vo.getStatus()); |
| | | userAccount.setUpdateTime(new Date()); |
| | | userAccountService.updateById(userAccount); |
| | | }else{ |
| | | return AjaxResult.error("更新用户属性失败!"); |
| | | } |
| | | // } else { |
| | | // return AjaxResult.error("更新用户名片失败!"); |
| | | // } |
| | | return AjaxResult.success("更新成功!"); |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | /** |
| | | * 更新账号属性 |
| | | * @param accountId 用户账号ID |
| | | * @return 操作结果 |
| | | */ |
| | | public AjaxResult updateAccountProperties(String accountId, UserAccountUpdateVo vo) { |
| | | |
| | | try (CloseableHttpClient httpClient = HttpClients.createDefault()) { |
| | | // 生成请求参数 |
| | | String nonce = UUID.randomUUID().toString().replace("-", ""); |
| | | String curTime = String.valueOf(System.currentTimeMillis() / 1000); |
| | | String checkSum = generateCheckSum(nonce, curTime); |
| | | |
| | | // 构建请求URL |
| | | String url = headPortraitUrl + "/im/v2/accounts/" + accountId; |
| | | |
| | | // 构建请求头 |
| | | HttpPatch httpPatch = new HttpPatch(url); |
| | | httpPatch.setHeader("Content-Type", "application/json;charset=utf-8"); |
| | | httpPatch.setHeader("AppKey", AppAuthConfig.DEFAULT_CONFIG.getAppKey()); |
| | | httpPatch.setHeader("Nonce", nonce); |
| | | httpPatch.setHeader("CurTime", curTime); |
| | | httpPatch.setHeader("CheckSum", checkSum); |
| | | |
| | | // 创建构建器实例 |
| | | DynamicRequestBodyBuilder builder = new DynamicRequestBodyBuilder(); |
| | | if(null != vo.getStatus() && vo.getStatus() == 1){ |
| | | builder.setEnabled(false); |
| | | builder.setNeedKick(true); |
| | | }else if(null != vo.getStatus() && vo.getStatus() == 0){ |
| | | builder.setEnabled(true); |
| | | }else if(StringUtils.isNotEmpty(vo.getPassword())){ |
| | | builder.setToken(vo.getPassword()); |
| | | } |
| | | // 只设置需要的字段 |
| | | String jsonBody = builder.build(); |
| | | httpPatch.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8)); |
| | | |
| | | // 执行请求 |
| | | HttpResponse response = httpClient.execute(httpPatch); |
| | | String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); |
| | | |
| | | // 解析响应 |
| | | NeteaseResponse neteaseResponse = objectMapper.readValue(responseString, NeteaseResponse.class); |
| | | |
| | | if (neteaseResponse.isSuccess()) { |
| | | return AjaxResult.success("账号属性更新成功"); |
| | | } else { |
| | | return AjaxResult.error("账号属性更新失败"); |
| | | } |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | return AjaxResult.error("请求网易云信API失败"); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 批量注册 |
| | | * @param dto |
| | | * @return |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Result batchRegister(RegisterDto dto) { |
| | | dto.setAccountType(1); |
| | | if(dto.getType() == 2){ |
| | | return register(dto); |
| | | }else{ |
| | | return batchRegister(dto.getNumber()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 同步批量注册 |
| | | * 注意:大批量(如超过1000)可能会造成事务过长、数据库连接占用较久,请根据实际情况调整批次大小或考虑异步方式 |
| | | * @param count 要注册的账号数量 |
| | | * @return 批量注册结果 |
| | | */ |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Result batchRegister(int count) { |
| | | if (count <= 0) { |
| | | return Result.error("注册数量必须大于0"); |
| | | } |
| | | |
| | | try { |
| | | // 1. 生成批量账号数据 |
| | | List<UserAccount> accountsToSave = generateBatchAccounts(count); |
| | | // 2. 批量插入数据库 (使用JDBC Batch,性能远高于MyBatis-Plus的saveBatch) |
| | | batchInsertAccounts(accountsToSave); |
| | | // 3. 批量注册云信账号 (并行处理) |
| | | Result yunxinResult = batchRegisterYunxinAccounts(accountsToSave); |
| | | |
| | | if (yunxinResult.getCode() != 200) { |
| | | // 云信注册失败,手动触发回滚 |
| | | TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); |
| | | return yunxinResult; |
| | | } |
| | | |
| | | return Result.success("成功批量注册 " + count + " 个账号",yunxinResult.getData()); |
| | | |
| | | } catch (Exception e) { |
| | | // 其他异常,触发回滚 |
| | | TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); |
| | | log.error("批量注册过程发生异常", e); |
| | | return Result.error("批量注册失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 生成批量的账号信息 |
| | | * @param count 需要生成的数量 |
| | | * @return 用户账号列表 |
| | | */ |
| | | private List<UserAccount> generateBatchAccounts(int count) { |
| | | List<UserAccount> accounts = new ArrayList<>(count); |
| | | Set<String> generatedAccounts = new HashSet<>(count); // 用于内存中去重 |
| | | Random random = new Random(); |
| | | String invitationCode = getInvitationCode(); |
| | | for (int i = 0; i < count; i++) { |
| | | String account; |
| | | do { |
| | | // 生成13开头的11位随机手机号作为账号 |
| | | account = "13" + String.format("%09d", random.nextInt(1000000000)); |
| | | } while (generatedAccounts.contains(account)); // 确保本次批量中唯一 |
| | | |
| | | generatedAccounts.add(account); |
| | | |
| | | UserAccount userAccount = new UserAccount(); |
| | | userAccount.setAccount(account); |
| | | userAccount.setPhoneNumber(account); |
| | | userAccount.setCloudMessageAccount(account); |
| | | userAccount.setPassword(ENCRYPTED_PASSWORD); // 使用预加密的密码 |
| | | userAccount.setCreateTime(new Date()); |
| | | userAccount.setNickname("用户_" + account.substring(7)); // 简单生成昵称 |
| | | userAccount.setAccountType(1); // 设置账号类型为1 |
| | | userAccount.setInvitationCode(invitationCode); |
| | | accounts.add(userAccount); |
| | | } |
| | | return accounts; |
| | | } |
| | | |
| | | /** |
| | | * 使用JDBC批量插入数据库,最高效的方式 |
| | | * @param accounts 待插入的用户账号列表 |
| | | */ |
| | | private void batchInsertAccounts(List<UserAccount> accounts) { |
| | | String sql = "INSERT INTO user_account (account, phone_number, cloud_message_account, password, create_time, nickname, account_type) VALUES (?, ?, ?, ?, ?, ?, ?)"; |
| | | |
| | | jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { |
| | | @Override |
| | | public void setValues(PreparedStatement ps, int i) throws SQLException { |
| | | UserAccount account = accounts.get(i); |
| | | ps.setString(1, account.getAccount()); |
| | | ps.setString(2, account.getPhoneNumber()); |
| | | ps.setString(3, account.getCloudMessageAccount()); |
| | | ps.setString(4, account.getPassword()); |
| | | ps.setTimestamp(5, new java.sql.Timestamp(account.getCreateTime().getTime())); |
| | | ps.setString(6, account.getNickname()); |
| | | ps.setInt(7, account.getAccountType()); |
| | | } |
| | | |
| | | @Override |
| | | public int getBatchSize() { |
| | | return accounts.size(); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 批量注册云信账号(并行调用单个接口) |
| | | * @param accounts 已存入本地数据库的账号列表 |
| | | * @return 注册结果 |
| | | */ |
| | | private Result batchRegisterYunxinAccounts(List<UserAccount> accounts) { |
| | | // 使用并行流并行调用云信接口 |
| | | List<CompletableFuture<YunxinApiResponse>> futures = accounts.parallelStream() |
| | | .map(account -> CompletableFuture.supplyAsync(() -> { |
| | | Map<String, String> paramMap = new HashMap<>(); |
| | | paramMap.put("accid", account.getAccount()); |
| | | paramMap.put("token", DEFAULT_PASSWORD); // 使用明文密码 |
| | | // 调用云信接口 |
| | | return yunxinClient.executeV1Api(YUNXIN_CREATE_PATH, paramMap); |
| | | })) |
| | | .collect(Collectors.toList()); |
| | | |
| | | // 等待所有异步操作完成,并获取结果 |
| | | List<YunxinApiResponse> responses = futures.stream() |
| | | .map(CompletableFuture::join) |
| | | .collect(Collectors.toList()); |
| | | |
| | | List<String> acccountList = new ArrayList<>(); |
| | | // 检查所有响应结果 |
| | | for (YunxinApiResponse response : responses) { |
| | | String data = response.getData(); |
| | | JSONObject json = JSONObject.parseObject(data); |
| | | int code = json.getIntValue("code"); |
| | | JSONObject info = json.getJSONObject("info"); |
| | | String accid = info.getString("accid"); |
| | | |
| | | if (code != 200) { |
| | | log.error("-----------云信账号注册失败:"+ ErrorCodeEnum.getByCode(code).getComment()+"----im信息:"+ErrorCodeEnum.getByCode(code).getDesc()); |
| | | return Result.error("云信注册失败:"+ErrorCodeEnum.getByCode(code).getComment()); |
| | | } |
| | | acccountList.add(accid); |
| | | } |
| | | return Result.success(acccountList); |
| | | } |
| | | |
| | | |
| | | public void addFriends(String accountId,String userAccid){ |
| | | try { |
| | | |
| | | Map<String, String> queryParams = null; |
| | | Map<String, Object> map = new HashMap<>(); |
| | | map.put("account_id",accountId); |
| | | map.put("friend_account_id",userAccid); |
| | | JSONObject jsonBody = JSONObject.parseObject(JSONObject.toJSONString(map)); |
| | | |
| | | |
| | | YunxinApiResponse apiResponse = yunxinClient.executeV2Api( |
| | | HttpMethod.POST, |
| | | FRIENDS_PATH, |
| | | FRIENDS_PATH, |
| | | queryParams, |
| | | jsonBody.toJSONString() |
| | | ); |
| | | // 检查所有响应结果 |
| | | String data = apiResponse.getData(); |
| | | JSONObject json = JSONObject.parseObject(data); |
| | | int code = json.getIntValue("code"); |
| | | if (code != 200) { |
| | | log.error("云信账号注册添加默认好友失败"); |
| | | } |
| | | } catch (YunxinSdkException e) { |
| | | // 云信调用异常时回滚事务 |
| | | e.printStackTrace(); |
| | | log.error("网易云信创建用户加群服务调用异常 traceId:"); |
| | | } catch (Exception e) { |
| | | // 其他异常同样回滚 |
| | | e.printStackTrace(); |
| | | log.error("创建用户加群过程发生未知异常", e); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | } |