package com.mzl.flower.service.impl.sms; 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.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; /** *

* 服务实现类 *

* * @author @TaoJie * @since 2024-12-25 */ @Service @Transactional @RequiredArgsConstructor public class SmsTaskServiceImpl extends ServiceImpl 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 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 smsUserDTOS = smsTaskDTO.getSmsUserDTOS(); SmsTaskDO smsTaskDO = new SmsTaskDO(); BeanUtils.copyProperties(smsTaskDTO, smsTaskDO); if(!CollectionUtils.isEmpty(smsUserDTOS)){ List userIds = smsUserDTOS.stream() .map(SmsUserDTO::getUserId) .collect(Collectors.toList()); List 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 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 smsUserDTOS = smsTaskDTO.getSmsUserDTOS(); if(!CollectionUtils.isEmpty(smsUserDTOS)){ List userIds = smsUserDTOS.stream() .map(SmsUserDTO::getUserId) .collect(Collectors.toList()); List 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 queryPage(SmsTaskQueryDTO dto, Page page) { List 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 lineList = Arrays.asList(lines); List 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(),smsTaskDTO.getId()); }); } @Override public List getSelectList(Long id) { List smsSelectVOList = null; SmsTaskDO smsTaskDO = smsTaskMapper.selectById(id); String ids = smsTaskDO.getUserIds(); if (StringUtils.isEmpty(ids)) { return smsSelectVOList; } else { String[] idArray = ids.split(";"); List 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 selectList = getSelectList(id); smsTaskVO.setSmsUserDTOS(selectList); return smsTaskVO; } return null; } private List createSmsTaskDetails(SmsTaskDO smsTaskDO, List 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 smsTaskDetailDOList, Long smsTemplateId,Long id) { 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); SmsTaskDO smsTaskDO = smsTaskMapper.selectById(id); smsTaskDO.setStatus(Constants.SMS_TASK_STATUS.complete.name()); smsTaskMapper.updateById(smsTaskDO); } } } }