| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |