CcExtNumServiceImpl.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. package com.ruoyi.cc.service.impl;
  2. import java.util.*;
  3. import com.auth0.jwt.JWT;
  4. import com.auth0.jwt.algorithms.Algorithm;
  5. import com.ruoyi.cc.service.ICcParamsService;
  6. import com.ruoyi.common.utils.StringUtils;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.stereotype.Service;
  10. import com.ruoyi.cc.mapper.CcExtNumMapper;
  11. import com.ruoyi.cc.domain.CcExtNum;
  12. import com.ruoyi.cc.service.ICcExtNumService;
  13. import com.ruoyi.common.core.text.Convert;
  14. /**
  15. * 【请填写功能名称】Service业务层处理
  16. *
  17. * @author ruoyi
  18. * @date 2024-12-22
  19. */
  20. @Service
  21. @Slf4j
  22. public class CcExtNumServiceImpl implements ICcExtNumService
  23. {
  24. @Autowired
  25. private CcExtNumMapper ccExtNumMapper;
  26. @Autowired
  27. private ICcParamsService ccParamsService;
  28. private String authTokenSecret;
  29. // 缓存最大分机号,初始值为0表示未初始化,需要从数据库查询
  30. // 使用volatile保证多线程可见性
  31. private volatile long cachedMaxExtNum = 0;
  32. // 用于同步获取和更新缓存的锁对象
  33. private final Object cacheLock = new Object();
  34. /**
  35. * 查询【请填写功能名称】
  36. *
  37. * @param extId 【请填写功能名称】主键
  38. * @return 【请填写功能名称】
  39. */
  40. @Override
  41. public CcExtNum selectCcExtNumByExtId(Long extId)
  42. {
  43. return ccExtNumMapper.selectCcExtNumByExtId(extId);
  44. }
  45. /**
  46. * 查询【请填写功能名称】列表
  47. *
  48. * @param ccExtNum 【请填写功能名称】
  49. * @return 【请填写功能名称】
  50. */
  51. @Override
  52. public List<CcExtNum> selectCcExtNumList(CcExtNum ccExtNum)
  53. {
  54. return ccExtNumMapper.selectCcExtNumList(ccExtNum);
  55. }
  56. /**
  57. * 新增【请填写功能名称】
  58. *
  59. * @param ccExtNum 【请填写功能名称】
  60. * @return 结果
  61. */
  62. @Override
  63. public int insertCcExtNum(CcExtNum ccExtNum)
  64. {
  65. return ccExtNumMapper.insertCcExtNum(ccExtNum);
  66. }
  67. /**
  68. * 修改【请填写功能名称】
  69. *
  70. * @param ccExtNum 【请填写功能名称】
  71. * @return 结果
  72. */
  73. @Override
  74. public int updateCcExtNum(CcExtNum ccExtNum)
  75. {
  76. return ccExtNumMapper.updateCcExtNum(ccExtNum);
  77. }
  78. /**
  79. * 批量删除【请填写功能名称】
  80. *
  81. * @param extIds 需要删除的【请填写功能名称】主键
  82. * @return 结果
  83. */
  84. @Override
  85. public int deleteCcExtNumByExtIds(String extIds)
  86. {
  87. return ccExtNumMapper.deleteCcExtNumByExtIds(Convert.toStrArray(extIds));
  88. }
  89. /**
  90. * 删除【请填写功能名称】信息
  91. *
  92. * @param extId 【请填写功能名称】主键
  93. * @return 结果
  94. */
  95. @Override
  96. public int deleteCcExtNumByExtId(Long extId)
  97. {
  98. return ccExtNumMapper.deleteCcExtNumByExtId(extId);
  99. }
  100. @Override
  101. public CcExtNum selectCcExtNumByExtNum(Long extNum) {
  102. List<CcExtNum> list = selectCcExtNumList(new CcExtNum().setExtNum(extNum));
  103. if (!list.isEmpty()) {
  104. return list.get(0);
  105. }
  106. return null;
  107. }
  108. @Override
  109. public CcExtNum selectCcExtNumByUserCode(String userCode) {
  110. List<CcExtNum> list = selectCcExtNumList(new CcExtNum().setUserCode(userCode));
  111. if (!list.isEmpty()) {
  112. return list.get(0);
  113. }
  114. return null;
  115. }
  116. @Override
  117. public String createToken(String extnum, String opnum, String groupId, String skillLevel, String projectId) {
  118. try {
  119. log.info("extnum:{}, opnum:{}, groupId:{}", extnum, opnum, groupId);
  120. authTokenSecret = ccParamsService.getParamValueByCode(
  121. "ws-server-auth-token-secret", "123456");
  122. //登录成功后生成JWT
  123. //JWT的header部分,该map可以是空的,因为有默认值{"alg":HS256,"typ":"JWT"}
  124. Map<String, Object> map = new HashMap<>();
  125. Calendar instance = Calendar.getInstance();
  126. instance.add(Calendar.HOUR, 24);
  127. return JWT.create()
  128. //添加头部
  129. .withHeader(map)
  130. //添加payload
  131. .withClaim("extnum", extnum)
  132. .withClaim("opnum", opnum)
  133. .withClaim("groupId", groupId)
  134. .withClaim("skillLevel", skillLevel)
  135. .withClaim("projectId", projectId)
  136. //设置过期时间
  137. .withExpiresAt(instance.getTime())
  138. //设置签名 密钥
  139. .sign(Algorithm.HMAC256(authTokenSecret));
  140. } catch (Exception err) {
  141. log.error("error:" + err.getMessage());
  142. }
  143. return null;
  144. }
  145. @Override
  146. public List<CcExtNum> selectUnBindCcExtNumList() {
  147. return ccExtNumMapper.selectUnBindCcExtNumList();
  148. }
  149. @Override
  150. public int cleanCcExtNumByUserCode(String loginName) {
  151. return ccExtNumMapper.cleanCcExtNumByUserCode(loginName);
  152. }
  153. @Override
  154. public int updateCcExtNumByUserCode(CcExtNum extNum) {
  155. return ccExtNumMapper.updateCcExtNumByUserCode(extNum);
  156. }
  157. @Override
  158. public List<CcExtNum> selectUnBindCcExtNumListPage(CcExtNum ccExtNum) {
  159. return ccExtNumMapper.selectUnBindCcExtNumListPage(ccExtNum);
  160. }
  161. @Override
  162. public int updateCompanyBindExtNum(CcExtNum extNum) {
  163. return ccExtNumMapper.updateCompanyBindExtNum(extNum);
  164. }
  165. @Override
  166. public int batchInsertCcExtNum(int count, String extPass, String userCode, Long startExtNum) {
  167. // ==================== 1. 参数校验(安全检查)====================
  168. if (count <= 0 || count > 10000) {
  169. throw new RuntimeException("生成数量必须在1-10000之间");
  170. }
  171. // 密码可以为空字符串,但不能为null
  172. if (extPass == null) {
  173. extPass = "";
  174. }
  175. // 密码长度校验(最多50个字符)
  176. if (extPass.length() > 50) {
  177. throw new RuntimeException("分机密码长度不能超过50个字符");
  178. }
  179. // 工号长度校验(最多32个字符)
  180. if (StringUtils.isNotEmpty(userCode) && userCode.length() > 32) {
  181. throw new RuntimeException("工号长度不能超过32个字符");
  182. }
  183. // ==================== 2. 工号唯一性校验 ====================
  184. if (StringUtils.isNotEmpty(userCode)) {
  185. CcExtNum checkUserCode = selectCcExtNumByUserCode(userCode);
  186. if (checkUserCode != null) {
  187. throw new RuntimeException("该工号已经绑定分机" + checkUserCode.getExtNum() + ",不允许重复绑定!");
  188. }
  189. }
  190. // ==================== 3. 确定起始分机号(使用缓存+数据库双重保障)====================
  191. long actualStartExtNum;
  192. long currentMaxExtNum;
  193. synchronized (cacheLock) {
  194. // 如果缓存为0,说明是第一次使用或系统刚重启,需要从数据库查询
  195. if (cachedMaxExtNum == 0) {
  196. Long dbMaxExtNum = ccExtNumMapper.selectMaxExtNum();
  197. currentMaxExtNum = (dbMaxExtNum == null) ? 0L : dbMaxExtNum;
  198. cachedMaxExtNum = currentMaxExtNum; // 更新缓存
  199. log.info("首次初始化分机号缓存,数据库最大分机号: {}", currentMaxExtNum);
  200. } else {
  201. currentMaxExtNum = cachedMaxExtNum;
  202. }
  203. // 确定实际起始分机号
  204. if (startExtNum == null || startExtNum <= 0) {
  205. actualStartExtNum = currentMaxExtNum + 1;
  206. if (actualStartExtNum <= 0) {
  207. actualStartExtNum = 1L; // 确保起始值至少为1
  208. }
  209. } else {
  210. // 校验用户指定的起始分机号不能小于当前最大值
  211. if (startExtNum <= currentMaxExtNum) {
  212. throw new RuntimeException("起始分机号(" + startExtNum + ")不能小于等于当前最大分机号(" + currentMaxExtNum + "),可选择不填起始分机号");
  213. }
  214. actualStartExtNum = startExtNum;
  215. }
  216. }
  217. // ==================== 4. 查询已存在的分机号(用于去重)====================
  218. List<Long> existingExtNums = ccExtNumMapper.selectExtNumsGreaterThan(actualStartExtNum - 1);
  219. Set<Long> existingExtNumSet = new HashSet<>(existingExtNums);
  220. // ==================== 5. 分批生成分机号并插入(防止内存溢出)====================
  221. int totalInserted = 0;
  222. long currentExtNum = actualStartExtNum;
  223. int remainingCount = count;
  224. // 分机号最大值限制(防止Long溢出)
  225. final long MAX_EXT_NUM = Long.MAX_VALUE - 10000; // 预留安全空间
  226. // 外层批次大小:每次生成1000个分机号
  227. final int GENERATE_BATCH_SIZE = 1000;
  228. // 内层插入批次大小:每次插入500条到数据库
  229. final int INSERT_BATCH_SIZE = 500;
  230. try {
  231. while (remainingCount > 0) {
  232. // 计算当前批次要生成的数量
  233. int currentBatchSize = Math.min(GENERATE_BATCH_SIZE, remainingCount);
  234. List<CcExtNum> currentBatchList = new ArrayList<>(currentBatchSize);
  235. // 生成当前批次的分机号
  236. int generatedInBatch = 0;
  237. long attempts = 0;
  238. // 防死循环:每个批次最多尝试 currentBatchSize * 10 次
  239. long maxAttemptsPerBatch = (long) currentBatchSize * 10L;
  240. while (generatedInBatch < currentBatchSize) {
  241. attempts++;
  242. // 【关键】安全检查1:防止死循环
  243. if (attempts > maxAttemptsPerBatch) {
  244. String errorMsg = String.format(
  245. "生成分机号失败:在区间[%d, %d]内可用分机号不足(已尝试%d次)。建议:1)删除部分已有分机号 2)指定更大的起始位置",
  246. actualStartExtNum, currentExtNum, attempts
  247. );
  248. log.error(errorMsg);
  249. throw new RuntimeException(errorMsg);
  250. }
  251. // 【关键】安全检查2:防止Long溢出
  252. if (currentExtNum <= 0 || currentExtNum >= MAX_EXT_NUM) {
  253. String errorMsg = String.format(
  254. "分机号已达到系统上限(currentExtNum=%d, MAX_EXT_NUM=%d),无法继续生成",
  255. currentExtNum, MAX_EXT_NUM
  256. );
  257. log.error(errorMsg);
  258. throw new RuntimeException(errorMsg);
  259. }
  260. // 如果当前分机号不存在,则添加到当前批次列表
  261. if (!existingExtNumSet.contains(currentExtNum)) {
  262. CcExtNum newExt = new CcExtNum();
  263. newExt.setExtNum(currentExtNum);
  264. newExt.setExtPass(extPass);
  265. newExt.setUserCode(userCode != null ? userCode : "");
  266. currentBatchList.add(newExt);
  267. generatedInBatch++;
  268. // 将新生成的分机号加入已存在集合,避免同一批次内重复
  269. existingExtNumSet.add(currentExtNum);
  270. }
  271. currentExtNum++;
  272. }
  273. // ==================== 6. 分批插入数据库 ====================
  274. if (!currentBatchList.isEmpty()) {
  275. for (int i = 0; i < currentBatchList.size(); i += INSERT_BATCH_SIZE) {
  276. int endIndex = Math.min(i + INSERT_BATCH_SIZE, currentBatchList.size());
  277. List<CcExtNum> insertBatchList = currentBatchList.subList(i, endIndex);
  278. int inserted = ccExtNumMapper.batchInsertCcExtNum(insertBatchList);
  279. totalInserted += inserted;
  280. }
  281. // 更新剩余计数
  282. remainingCount -= generatedInBatch;
  283. // 【关键】更新缓存的最大分机号(在同步块中保证线程安全)
  284. synchronized (cacheLock) {
  285. long newMaxExtNum = currentBatchList.get(currentBatchList.size() - 1).getExtNum();
  286. if (newMaxExtNum > cachedMaxExtNum) {
  287. cachedMaxExtNum = newMaxExtNum;
  288. log.debug("更新分机号缓存为: {}", cachedMaxExtNum);
  289. }
  290. }
  291. // 记录进度日志
  292. log.info("批量生成分机号进度: {}/{}", totalInserted, count);
  293. }
  294. }
  295. log.info("批量生成分机号完成,共生成 {} 条记录", totalInserted);
  296. return totalInserted;
  297. } catch (RuntimeException e) {
  298. log.error("批量生成分机号失败: {}", e.getMessage(), e);
  299. throw e;
  300. } catch (Exception e) {
  301. log.error("批量生成分机号发生未知错误", e);
  302. throw new RuntimeException("批量生成分机号失败: " + e.getMessage(), e);
  303. }
  304. }
  305. @Override
  306. public int companyBatchUnbindSipExt(Set<Long> extNums) {
  307. return ccExtNumMapper.companyBatchUnbindSipExt(extNums);
  308. }
  309. }