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.*; 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; @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 generatingAccounts = Collections.newSetFromMap(new ConcurrentHashMap<>()); // 使用原子计数器优化账号生成 private final AtomicLong lastTimestamp = new AtomicLong(0); private final AtomicLong sequence = new AtomicLong(0); private static final String DEFAULT_PASSWORD = "123456";//批量注册密码 private static final String ENCRYPTED_PASSWORD = SymmetricCryptoUtil.encryptPassword(DEFAULT_PASSWORD); // 密码加密一次,多次使用 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) { // 验证手机号是否已存在 List accounts = userAccountService.list( new LambdaQueryWrapper<>(UserAccount.class) .eq(UserAccount::getAccount, dto.getAccount()) ); if (!CollectionUtils.isEmpty(accounts)) { return Result.error("账号已被注册!"); } if(dto.getAccountType() == 0 && StringUtils.isEmpty(dto.getInvitationCode())){ return Result.error("邀请码不能为空!"); } long count = invitationBlacklistService.count(new LambdaQueryWrapper() .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() .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 paramMap = new HashMap<>(); paramMap.put("accid", dto.getAccount()); if(StringUtils.isNotEmpty(dto.getNikeName())){ paramMap.put("name", dto.getNikeName()); } paramMap.put("token", dto.getPassword()); 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); } //默认添加邀请人为好友 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().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 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() .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); } /** * 优化的账号生成方法,使用雪花算法变体提高并发性能 */ public long generateUniqueCloudMessageAccount() { final int maxAttempts = 10; // 减少尝试次数,提高效率 for (int attempt = 0; attempt < maxAttempts; attempt++) { long account = generateSnowflakeLikeId(); // 使用并发集合检查是否正在生成相同账号 if (generatingAccounts.add(account)) { try { // 检查数据库是否存在 boolean exists = userAccountService.lambdaQuery() .eq(UserAccount::getCloudMessageAccount, account) .exists(); if (!exists) { return account; } } finally { // 无论成功与否,都从集合中移除 generatingAccounts.remove(account); } } // 短暂等待后重试 try { Thread.sleep(ThreadLocalRandom.current().nextInt(10, 50)); } catch (InterruptedException e) { e.printStackTrace(); Thread.currentThread().interrupt(); throw new RuntimeException("生成账号被中断", e); } } throw new RuntimeException("无法在" + maxAttempts + "次尝试内生成唯一账号"); } /** * 基于雪花算法的ID生成器,提高并发性能 */ private long generateSnowflakeLikeId() { long currentTime = System.currentTimeMillis(); long timestamp = currentTime % 100_000_000L; // 取时间戳的后8位 // 使用原子操作确保序列号递增 long seq; long lastTime; do { lastTime = lastTimestamp.get(); if (currentTime == lastTime) { seq = sequence.incrementAndGet() % 100; } else { sequence.set(0); lastTimestamp.set(currentTime); seq = 0; } } while (!lastTimestamp.compareAndSet(lastTime, currentTime)); // 组合时间戳和序列号,生成9位ID return timestamp * 100 + seq; } /** * 更新用户名片 * @param accountId 用户账号 * @return 操作结果 */ public Map updateUserAvatar(String accountId, UpdateUserBusinessDto dto) { Map 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 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() .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(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 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 generateBatchAccounts(int count) { List accounts = new ArrayList<>(count); Set 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 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 accounts) { // 使用并行流并行调用云信接口 List> futures = accounts.parallelStream() .map(account -> CompletableFuture.supplyAsync(() -> { Map paramMap = new HashMap<>(); paramMap.put("accid", account.getAccount()); paramMap.put("token", DEFAULT_PASSWORD); // 使用明文密码 // 调用云信接口 return yunxinClient.executeV1Api(YUNXIN_CREATE_PATH, paramMap); })) .collect(Collectors.toList()); // 等待所有异步操作完成,并获取结果 List responses = futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList()); List 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 queryParams = null; Map 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); } } }