From b9903ead016b8b1aa68eb04b48fca3b53fdab0d3 Mon Sep 17 00:00:00 2001 From: cloudroam <cloudroam> Date: 星期一, 30 十二月 2024 10:42:43 +0800 Subject: [PATCH] Merge remote-tracking branch 'origin/master-v4' into master-v4 --- src/main/java/com/mzl/flower/service/impl/sms/SmsTaskServiceImpl.java | 430 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 426 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/mzl/flower/service/impl/sms/SmsTaskServiceImpl.java b/src/main/java/com/mzl/flower/service/impl/sms/SmsTaskServiceImpl.java index e97edc3..f3af2d8 100644 --- a/src/main/java/com/mzl/flower/service/impl/sms/SmsTaskServiceImpl.java +++ b/src/main/java/com/mzl/flower/service/impl/sms/SmsTaskServiceImpl.java @@ -1,20 +1,442 @@ package com.mzl.flower.service.impl.sms; -import com.mzl.flower.entity.SmsTaskDO; -import com.mzl.flower.mapper.SmsTaskMapper; -import com.mzl.flower.service.sms.SmsTaskService; +import com.aliyun.oss.ClientException; +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.OSSException; +import com.aliyun.oss.model.GetObjectRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mzl.flower.config.OssProperties; +import com.mzl.flower.config.exception.ValidationException; +import com.mzl.flower.config.security.SecurityUtils; +import com.mzl.flower.constant.Constants; +import com.mzl.flower.dto.request.sms.SmsTaskDTO; +import com.mzl.flower.dto.request.sms.SmsTaskQueryDTO; +import com.mzl.flower.dto.request.sms.SmsUserDTO; +import com.mzl.flower.dto.response.sms.SmsSelectVO; +import com.mzl.flower.dto.response.sms.SmsTaskVO; +import com.mzl.flower.entity.SmsTaskDO; +import com.mzl.flower.entity.SmsTaskDetailDO; +import com.mzl.flower.entity.SmsTemplateDO; +import com.mzl.flower.mapper.SmsTaskDetailMapper; +import com.mzl.flower.mapper.SmsTaskMapper; +import com.mzl.flower.mapper.SmsTemplateMapper; +import com.mzl.flower.mapper.system.UserMapper; +import com.mzl.flower.service.sms.SmsTaskDetailService; +import com.mzl.flower.service.sms.SmsTaskService; +import com.mzl.flower.service.system.UserService; +import com.mzl.flower.utils.SmsUtil; +import lombok.RequiredArgsConstructor; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * <p> - * 服务实现类 + * 服务实现类 * </p> * * @author @TaoJie * @since 2024-12-25 */ @Service +@Transactional +@RequiredArgsConstructor public class SmsTaskServiceImpl extends ServiceImpl<SmsTaskMapper, SmsTaskDO> implements SmsTaskService { + private final OssProperties ossProperties; + private final SmsTaskMapper smsTaskMapper; + + private final UserMapper userMapper; + + private final SmsTemplateMapper smsTemplateMapper; + + private final SmsTaskDetailMapper smsTaskDetailMapper; + private final SmsTaskDetailService smsTaskDetailService; + private static final Pattern PHONE_NUMBER_PATTERN = Pattern.compile("^1[3-9]\\d{9}$"); + private static final String PHONE_REGEX = "^1[3-9]\\d{9}$"; + + public static boolean isValidPhoneNumber(String phoneNumber) { + if (phoneNumber == null || phoneNumber.length() != 11) { + return false; + } + + Pattern pattern = Pattern.compile(PHONE_REGEX); + Matcher matcher = pattern.matcher(phoneNumber); + + return matcher.matches(); + } + + @Override + public void saveSmsTask(SmsTaskDTO smsTaskDTO) throws IOException { + //校验 + if (StringUtils.isEmpty(smsTaskDTO.getName())) { + throw new ValidationException("短信名称不能为空"); + } + + if (StringUtils.isEmpty(smsTaskDTO.getSmsTemplateId())) { + throw new ValidationException("短信模板不能为空"); + } + + if (StringUtils.isEmpty(smsTaskDTO.getType())) { + throw new ValidationException("接收号码类型不能为空"); + } + + if (Constants.SMS_RECEIVE_TYPE.INPUT.name().equals(smsTaskDTO.getType()) && StringUtils.isEmpty(smsTaskDTO.getPhones())) { + throw new ValidationException("手机号不能为空"); + } + + if (Constants.SMS_RECEIVE_TYPE.IMPORT.name().equals(smsTaskDTO.getType()) && StringUtils.isEmpty(smsTaskDTO.getFileUrl())) { + throw new ValidationException("导入文件不能为空"); + } + + if (Constants.SMS_RECEIVE_TYPE.INPUT.name().equals(smsTaskDTO.getType()) ) { + + //解析手机号,包含不同平台的换行符 + String text = smsTaskDTO.getPhones(); + + // 使用正则表达式匹配所有类型的换行符 + String[] lines = text.split("\\r?\\n|\\r"); + + // 将数组转换为 List + List<String> lineList = Arrays.asList(lines); + lineList.forEach(l -> { + boolean validPhoneNumber = isValidPhoneNumber(l); + if (!validPhoneNumber) { + throw new ValidationException(l + "不是合法的手机号"); + } + }); + smsTaskDTO.setNum((long) lineList.size()); + } + if (Constants.SMS_RECEIVE_TYPE.IMPORT.name().equals(smsTaskDTO.getType())) { + dealImportExcel(smsTaskDTO); + } + List<SmsUserDTO> smsUserDTOS = smsTaskDTO.getSmsUserDTOS(); + SmsTaskDO smsTaskDO = new SmsTaskDO(); + BeanUtils.copyProperties(smsTaskDTO, smsTaskDO); + if(!CollectionUtils.isEmpty(smsUserDTOS)){ + List<String> userIds = smsUserDTOS.stream() + .map(SmsUserDTO::getUserId) + .collect(Collectors.toList()); + + List<String> userPhones = smsUserDTOS.stream() + .map(SmsUserDTO::getUserPhone) + .collect(Collectors.toList()); + if(!CollectionUtils.isEmpty(userPhones)){ + String phones = userPhones.stream() + .map(Object::toString) // 确保每个元素都转换为字符串 + .collect(Collectors.joining("\n")); // 使用换行符连接字符串 + smsTaskDTO.setPhones(phones); // 假设有一个setPhones方法用于设置phones字段 + } + if (!CollectionUtils.isEmpty(userIds)) { + String userIdInfos = userIds.stream().map(Object::toString) // 确保每个元素都转换为字符串 + .collect(Collectors.joining(";")); // 使用换行符连接字符串 + smsTaskDO.setUserIds(userIdInfos); + smsTaskDTO.setNum((long) userIds.size()); + } + } + smsTaskDO.setStatus(Constants.SMS_TASK_STATUS.wait_publish.name()); + smsTaskDO.setPhones(smsTaskDTO.getPhones()); + smsTaskDO.create(SecurityUtils.getUserId()); + smsTaskMapper.insert(smsTaskDO); + } + + private void dealImportExcel(SmsTaskDTO smsTaskDTO) throws IOException { + String fileUrlMessage = ""; + // 创建ObjectMapper实例 + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode rootNode = objectMapper.readTree(smsTaskDTO.getFileUrl()); + if (rootNode.isArray()) { + JsonNode firstElement = rootNode.get(0); + if (firstElement.has("url")) { + fileUrlMessage = firstElement.get("url").asText(); + } else { + throw new ValidationException("URL字段不存在"); + } + } else { + throw new ValidationException("JSON数组为空或不是数组"); + } + + String endPoint = ossProperties.getEndpoint(); + String accessKeyId = ossProperties.getKeyid(); + String accessKeySecret = ossProperties.getKeysecret(); + String bucketName = ossProperties.getBucketname(); + + OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret); + try { + // 下载Excel文件到本地临时文件 + File tempFile = File.createTempFile("temp", ".xlsx"); + String fileUrl = fileUrlMessage; + String objectKey = fileUrl.replaceFirst("^https?://[^/]+/", ""); // 去掉协议部分 + ossClient.getObject(new GetObjectRequest(bucketName, objectKey), tempFile); + + // 解析Excel文件 + try (FileInputStream inputStream = new FileInputStream(tempFile); + Workbook workbook = new XSSFWorkbook(inputStream)) { + + Sheet sheet = workbook.getSheetAt(0); // 获取第一个工作表 + int rowCount = sheet.getPhysicalNumberOfRows(); + + if (rowCount > 101) { + throw new ValidationException("一次导入手机号最多100行"); + } + + boolean isValid = true; + StringBuffer message = new StringBuffer(); + StringBuffer phones = new StringBuffer(); + for (int i = 1; i < rowCount; i++) { // 跳过标题行,从第二行开始 + Row row = sheet.getRow(i); + if (row != null) { + Cell cell = row.getCell(0); // 假设手机号在第一列 + if (cell != null ) { + String phoneNumber = ""; + if (cell.getCellType() == CellType.STRING) { + phoneNumber = cell.getStringCellValue(); + } else if (cell.getCellType() == CellType.NUMERIC) { + // 将数字类型的手机号转换为字符串 + phoneNumber = String.valueOf((long) cell.getNumericCellValue()); + } + if (!PHONE_NUMBER_PATTERN.matcher(phoneNumber).matches()) { + message.append("第" + (i + 1) + "行手机号" + phoneNumber + "格式不正确"); + isValid = false; + break; // 退出循环 + } else { + phones.append(phoneNumber).append("\n"); + } + } else { + message.append("第" + (i + 1)+ "行上的单元格为空或无效 "); + isValid = false; + break; // 退出循环 + } + } else { + message.append("空行 " + (i + 1)); + isValid = false; + break; // 退出循环 + } + } + + if (!isValid) { + throw new ValidationException(message.toString()); + } else { + smsTaskDTO.setPhones(phones.toString()); + smsTaskDTO.setNum((long) rowCount - 1); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + // 删除临时文件 + if (tempFile.exists()) { + tempFile.delete(); + } + } + } catch (OSSException | ClientException | IOException e) { + e.printStackTrace(); + } finally { + ossClient.shutdown(); + } + } + + @Override + public void updateSmsTask(SmsTaskDTO smsTaskDTO) throws IOException { + SmsTaskDO smsTaskDO = smsTaskMapper.selectById(smsTaskDTO.getId()); + if (!smsTaskDO.getStatus().equals(Constants.SMS_TASK_STATUS.wait_publish.name())) { + throw new ValidationException("非待发布的任务不可编辑"); + } + + if (StringUtils.isEmpty(smsTaskDTO.getName())) { + throw new ValidationException("短信名称不能为空"); + } + + if (StringUtils.isEmpty(smsTaskDTO.getSmsTemplateId())) { + throw new ValidationException("短信模板不能为空"); + } + + if (StringUtils.isEmpty(smsTaskDTO.getType())) { + throw new ValidationException("接收号码类型不能为空"); + } + + if (Constants.SMS_RECEIVE_TYPE.INPUT.name().equals(smsTaskDTO.getType()) && StringUtils.isEmpty(smsTaskDTO.getPhones())) { + throw new ValidationException("手机号不能为空"); + } + + if (Constants.SMS_RECEIVE_TYPE.IMPORT.name().equals(smsTaskDTO.getType()) && StringUtils.isEmpty(smsTaskDTO.getFileUrl())) { + throw new ValidationException("导入文件不能为空"); + } + + if (Constants.SMS_RECEIVE_TYPE.INPUT.name().equals(smsTaskDTO.getType()) ) { + + //解析手机号,包含不同平台的换行符 + String text = smsTaskDTO.getPhones(); + + // 使用正则表达式匹配所有类型的换行符 + String[] lines = text.split("\\r?\\n|\\r"); + + // 将数组转换为 List + List<String> lineList = Arrays.asList(lines); + lineList.forEach(l -> { + boolean validPhoneNumber = isValidPhoneNumber(l); + if (!validPhoneNumber) { + throw new ValidationException(l + "不是合法的手机号"); + } + }); + smsTaskDTO.setNum((long) lineList.size()); + } + + if(Constants.SMS_RECEIVE_TYPE.IMPORT.name().equals(smsTaskDTO.getType()) && !smsTaskDTO.getFileUrl().equals(smsTaskDO.getFileUrl())){ + dealImportExcel(smsTaskDTO); + } + List<SmsUserDTO> smsUserDTOS = smsTaskDTO.getSmsUserDTOS(); + if(!CollectionUtils.isEmpty(smsUserDTOS)){ + List<String> userIds = smsUserDTOS.stream() + .map(SmsUserDTO::getUserId) + .collect(Collectors.toList()); + + List<String> userPhones = smsUserDTOS.stream() + .map(SmsUserDTO::getUserPhone) + .collect(Collectors.toList()); + if(!CollectionUtils.isEmpty(userPhones)){ + String phones = userPhones.stream() + .map(Object::toString) // 确保每个元素都转换为字符串 + .collect(Collectors.joining("\n")); // 使用换行符连接字符串 + smsTaskDTO.setPhones(phones); // 假设有一个setPhones方法用于设置phones字段 + } + if (!CollectionUtils.isEmpty(userIds)) { + String userIdInfos = userIds.stream().map(Object::toString) // 确保每个元素都转换为字符串 + .collect(Collectors.joining(";")); // 使用换行符连接字符串 + smsTaskDO.setUserIds(userIdInfos); + smsTaskDTO.setNum((long) userIds.size()); + } + } + BeanUtils.copyProperties(smsTaskDTO, smsTaskDO,"userIds"); + smsTaskDO.update(SecurityUtils.getUserId()); + smsTaskDO.setPhones(smsTaskDTO.getPhones()); + smsTaskMapper.updateById(smsTaskDO); + } + + @Override + public void deleteSmsTask(Long id) { + SmsTaskDO smsTaskDO = smsTaskMapper.selectById(id); + if (smsTaskDO == null) { + throw new ValidationException("短信任务ID不存在"); + } + if(!smsTaskDO.getStatus().equals(Constants.SMS_TASK_STATUS.wait_publish.name())){ + throw new ValidationException("非待发布的任务不可删除"); + } + smsTaskMapper.deleteById(id); + } + + @Override + public Page<SmsTaskVO> queryPage(SmsTaskQueryDTO dto, Page page) { + List<SmsTaskVO> list = smsTaskMapper.queryPage(dto, page); + page.setRecords(list); + return page; + } + + @Override + public void publishSmsTask(SmsTaskDTO smsTaskDTO) { + if (StringUtils.isEmpty(smsTaskDTO.getId())) { + throw new ValidationException("任务ID不能为空"); + } + SmsTaskDO smsTaskDO = smsTaskMapper.selectById(smsTaskDTO.getId()); + if(StringUtils.isEmpty(smsTaskDO.getPhones())){ + throw new ValidationException("任务手机号不能为空"); + } + //解析手机号,包含不同平台的换行符 + String text = smsTaskDO.getPhones(); + + // 使用正则表达式匹配所有类型的换行符 + String[] lines = text.split("\\r?\\n|\\r"); + + // 将数组转换为 List + List<String> lineList = Arrays.asList(lines); + List<SmsTaskDetailDO> smsTaskDetailDOList = createSmsTaskDetails(smsTaskDO, lineList); + smsTaskDO.setStatus(Constants.SMS_TASK_STATUS.in_execution.name()); + smsTaskDO.update(SecurityUtils.getUserId()); + smsTaskMapper.updateById(smsTaskDO); + // 异步保存任务明细信息并发送短信 + CompletableFuture.runAsync(() -> { + smsTaskDetailService.saveBatch(smsTaskDetailDOList); + sendSmsToAll(smsTaskDetailDOList, smsTaskDO.getSmsTemplateId()); + }); + } + + @Override + public List<SmsSelectVO> getSelectList(Long id) { + List<SmsSelectVO> smsSelectVOList = null; + SmsTaskDO smsTaskDO = smsTaskMapper.selectById(id); + String ids = smsTaskDO.getUserIds(); + if (StringUtils.isEmpty(ids)) { + return smsSelectVOList; + } else { + String[] idArray = ids.split(";"); + List<String> idList = Arrays.asList(idArray); + return userMapper.getSelectList(idList); + } + } + + @Override + public SmsTaskVO getDetailById(Long id) { + SmsTaskDO smsTaskDO = smsTaskMapper.selectById(id); + SmsTaskVO smsTaskVO=new SmsTaskVO(); + BeanUtils.copyProperties(smsTaskDO,smsTaskVO); + if(!ObjectUtils.isEmpty(smsTaskDO)){ + List<SmsSelectVO> selectList = getSelectList(id); + smsTaskVO.setSmsUserDTOS(selectList); + return smsTaskVO; + } + return null; + } + + private List<SmsTaskDetailDO> createSmsTaskDetails(SmsTaskDO smsTaskDO, List<String> phoneNumbers) { + return phoneNumbers.stream().map(phone -> { + SmsTaskDetailDO detail = new SmsTaskDetailDO(); + detail.setSmsTaskId(smsTaskDO.getId()); + detail.setSmsTemplateId(smsTaskDO.getSmsTemplateId()); + detail.setPhone(phone); + detail.create(); + return detail; + }).collect(Collectors.toList()); + } + + private void sendSmsToAll(List<SmsTaskDetailDO> smsTaskDetailDOList, Long smsTemplateId) { + SmsTemplateDO smsTemplateDO = smsTemplateMapper.selectById(smsTemplateId); + String templateCode = smsTemplateDO.getCode(); + for (SmsTaskDetailDO detail : smsTaskDetailDOList) { + try { + SendSmsResponse sendSmsResponse = SmsUtil.sendSms(detail.getPhone(), templateCode, null); + if("OK".equals(sendSmsResponse.getCode())){ + detail.setResult(Constants.SMS_SEND_RESULT.success.name()); + }else{ + detail.setFailReason(sendSmsResponse.getMessage()); + detail.setResult(Constants.SMS_SEND_RESULT.failure.name()); + } + detail.setResponseResult(sendSmsResponse.toString()); + } catch (Exception e) { + detail.setResult(Constants.SMS_SEND_RESULT.failure.name()); + System.err.println("Failed to send SMS to " + detail.getPhone() + ": " + e.getMessage()); + } finally { + //无论如何都更新结果 + smsTaskDetailMapper.updateById(detail); + } + } + } } -- Gitblit v1.9.3