From f400a93bdd3f06e3e5e9e6ae50b0c5e652d0c74a Mon Sep 17 00:00:00 2001 From: andy <594580820@qq.com> Date: Mon, 3 Aug 2026 16:06:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9Epushplus=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E6=8E=A8=E9=80=81=E6=96=B0=E6=8A=A5=E5=A4=87=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现pushplus微信推送新报备通知的全套功能: - 新增通知服务接口与实现类 - 配置异步线程池避免阻塞主业务 - 补充application.yml推送配置项 - 添加管理员测试推送API接口 - 在报备创建事务提交后触发通知,确保仅成功提交的报备发送通知 --- .../java/com/bycrm/config/AsyncConfig.java | 34 +++++ .../controller/SystemConfigController.java | 21 ++- .../java/com/bycrm/service/NotifyService.java | 22 +++ .../bycrm/service/impl/NotifyServiceImpl.java | 129 ++++++++++++++++++ .../bycrm/service/impl/ReportServiceImpl.java | 18 ++- backend/src/main/resources/application.yml | 8 ++ 6 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/com/bycrm/config/AsyncConfig.java create mode 100644 backend/src/main/java/com/bycrm/service/NotifyService.java create mode 100644 backend/src/main/java/com/bycrm/service/impl/NotifyServiceImpl.java diff --git a/backend/src/main/java/com/bycrm/config/AsyncConfig.java b/backend/src/main/java/com/bycrm/config/AsyncConfig.java new file mode 100644 index 0000000..1490be8 --- /dev/null +++ b/backend/src/main/java/com/bycrm/config/AsyncConfig.java @@ -0,0 +1,34 @@ +package com.bycrm.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * 异步任务配置:为消息推送等异步操作提供线程池。 + */ +@Configuration +@EnableAsync +public class AsyncConfig { + + /** + * pushplus 推送专用线程池。 + * 满载时降级为调用线程同步执行(CallerRunsPolicy),宁可变慢也不丢弃推送任务。 + */ + @Bean("pushplusExecutor") + public Executor pushplusExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(4); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("pushplus-"); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.initialize(); + return executor; + } +} diff --git a/backend/src/main/java/com/bycrm/controller/SystemConfigController.java b/backend/src/main/java/com/bycrm/controller/SystemConfigController.java index b768d69..0019781 100644 --- a/backend/src/main/java/com/bycrm/controller/SystemConfigController.java +++ b/backend/src/main/java/com/bycrm/controller/SystemConfigController.java @@ -4,6 +4,7 @@ import com.bycrm.common.Result; import com.bycrm.entity.SystemConfig; import com.bycrm.entity.User; import com.bycrm.mapper.UserMapper; +import com.bycrm.service.NotifyService; import com.bycrm.service.SystemConfigService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -23,11 +24,14 @@ public class SystemConfigController { private final SystemConfigService systemConfigService; private final UserMapper userMapper; + private final NotifyService notifyService; public SystemConfigController(SystemConfigService systemConfigService, - UserMapper userMapper) { + UserMapper userMapper, + NotifyService notifyService) { this.systemConfigService = systemConfigService; this.userMapper = userMapper; + this.notifyService = notifyService; } /** @@ -104,4 +108,19 @@ public class SystemConfigController { systemConfigService.batchUpdateConfigs(configs); return Result.success(); } + + /** + * 测试 pushplus 推送配置(仅管理员) + */ + @ApiOperation("测试 pushplus 推送") + @PostMapping("/pushplus/test") + public Result testPushplus(HttpServletRequest request) { + Long currentUserId = (Long) request.getAttribute("currentUserId"); + User currentUser = userMapper.selectById(currentUserId); + if (currentUser == null || currentUser.getRole() != 0) { + return Result.error("只有管理员才能测试推送"); + } + notifyService.sendTest(); + return Result.success(); + } } diff --git a/backend/src/main/java/com/bycrm/service/NotifyService.java b/backend/src/main/java/com/bycrm/service/NotifyService.java new file mode 100644 index 0000000..ed8d4a2 --- /dev/null +++ b/backend/src/main/java/com/bycrm/service/NotifyService.java @@ -0,0 +1,22 @@ +package com.bycrm.service; + +import com.bycrm.entity.Report; + +/** + * 消息通知服务(pushplus 微信推送) + */ +public interface NotifyService { + + /** + * 报备创建后通知管理员。 + * + * @param report 新建的报备(需含 id 及各业务字段) + * @param dealerName 经销商名称 + */ + void notifyNewReport(Report report, String dealerName); + + /** + * 发送一条测试消息,用于验证 pushplus 配置是否正确。 + */ + void sendTest(); +} diff --git a/backend/src/main/java/com/bycrm/service/impl/NotifyServiceImpl.java b/backend/src/main/java/com/bycrm/service/impl/NotifyServiceImpl.java new file mode 100644 index 0000000..2eddc97 --- /dev/null +++ b/backend/src/main/java/com/bycrm/service/impl/NotifyServiceImpl.java @@ -0,0 +1,129 @@ +package com.bycrm.service.impl; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; +import com.bycrm.entity.Report; +import com.bycrm.service.NotifyService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +/** + * pushplus 消息推送实现。 + * 异步执行(pushplusExecutor),任何异常仅记日志,绝不影响主业务。 + */ +@Service +public class NotifyServiceImpl implements NotifyService { + + private static final Logger log = LoggerFactory.getLogger(NotifyServiceImpl.class); + private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Value("${pushplus.enabled:false}") + private boolean enabled; + + @Value("${pushplus.token:}") + private String token; + + @Value("${pushplus.topic:}") + private String topic; + + @Value("${pushplus.url:https://www.pushplus.plus/send}") + private String url; + + @Value("${pushplus.approval-url:}") + private String approvalUrl; + + @Override + @Async("pushplusExecutor") + public void notifyNewReport(Report report, String dealerName) { + if (!enabled || token == null || token.trim().isEmpty()) { + return; + } + String title = "新报备申请:" + safe(report.getSchoolName()); + String content = buildReportContent(report, dealerName); + send(title, content); + } + + @Override + @Async("pushplusExecutor") + public void sendTest() { + if (!enabled || token == null || token.trim().isEmpty()) { + log.warn("pushplus 未启用或 token 为空,测试推送已跳过。请在 application.yml 配置 pushplus.enabled/token"); + return; + } + String content = "

泊云智销通推送测试

收到本消息说明 pushplus 配置正常,经销商提交报备时您将收到通知。

"; + send("泊云智销通推送测试", content); + } + + /** + * 实际调用 pushplus 接口。全程容错,失败仅记日志。 + */ + private void send(String title, String content) { + try { + Map body = new HashMap<>(); + body.put("token", token); + body.put("title", title); + body.put("content", content); + body.put("template", "html"); + if (topic != null && !topic.trim().isEmpty()) { + body.put("topic", topic); + } + + String json = JSONUtil.toJsonStr(body); + HttpResponse response = HttpRequest.post(url) + .header("Content-Type", "application/json") + .body(json) + .timeout(5000) + .execute(); + log.info("pushplus 推送结果: status={}, body={}", response.getStatus(), response.body()); + } catch (Exception e) { + log.warn("pushplus 推送失败(不影响业务): {}", e.getMessage()); + } + } + + private String buildReportContent(Report report, String dealerName) { + StringBuilder sb = new StringBuilder(); + sb.append("

有新的报备申请待审核

"); + sb.append(""); + row(sb, "报备编号", String.valueOf(report.getId())); + row(sb, "经销商", safe(dealerName)); + row(sb, "学校名称", safe(report.getSchoolName())); + row(sb, "所属产品", safe(report.getProduct())); + row(sb, "项目类型", safe(report.getProjectType())); + row(sb, "负责人", safe(report.getContactPerson())); + row(sb, "联系电话", safe(report.getContactPhone())); + row(sb, "报备说明", safe(report.getDescription())); + LocalDateTime createdAt = report.getCreatedAt(); + row(sb, "提交时间", createdAt == null ? "-" : createdAt.format(FMT)); + sb.append("
"); + if (approvalUrl != null && !approvalUrl.trim().isEmpty()) { + sb.append("

请跳转 ") + .append(approvalUrl) + .append(" 去审批处理。

"); + } + return sb.toString(); + } + + private void row(StringBuilder sb, String label, String value) { + sb.append("") + .append(label) + .append("") + .append(value) + .append(""); + } + + private String safe(String s) { + return s == null ? "-" : s; + } +} diff --git a/backend/src/main/java/com/bycrm/service/impl/ReportServiceImpl.java b/backend/src/main/java/com/bycrm/service/impl/ReportServiceImpl.java index 36dd425..6f46db4 100644 --- a/backend/src/main/java/com/bycrm/service/impl/ReportServiceImpl.java +++ b/backend/src/main/java/com/bycrm/service/impl/ReportServiceImpl.java @@ -15,12 +15,15 @@ import com.bycrm.mapper.DealerMapper; import com.bycrm.mapper.ReportMapper; import com.bycrm.mapper.SchoolMapper; import com.bycrm.mapper.UserMapper; +import com.bycrm.service.NotifyService; import com.bycrm.service.ReportService; import com.bycrm.service.SystemConfigService; import com.bycrm.vo.ReportVO; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import java.time.LocalDate; import java.time.LocalDateTime; @@ -39,17 +42,20 @@ public class ReportServiceImpl implements ReportService { private final UserMapper userMapper; private final DealerMapper dealerMapper; private final SystemConfigService systemConfigService; + private final NotifyService notifyService; public ReportServiceImpl(ReportMapper reportMapper, SchoolMapper schoolMapper, UserMapper userMapper, DealerMapper dealerMapper, - SystemConfigService systemConfigService) { + SystemConfigService systemConfigService, + NotifyService notifyService) { this.reportMapper = reportMapper; this.schoolMapper = schoolMapper; this.userMapper = userMapper; this.dealerMapper = dealerMapper; this.systemConfigService = systemConfigService; + this.notifyService = notifyService; } @Override @@ -162,6 +168,16 @@ public class ReportServiceImpl implements ReportService { report.setUpdatedAt(LocalDateTime.now()); reportMapper.insert(report); + + // 事务提交后异步推送通知管理员(事务回滚则不推送;推送失败不影响报备提交) + Dealer notifyDealer = dealerMapper.selectById(currentUser.getDealerId()); + final String dealerName = notifyDealer != null ? notifyDealer.getName() : ""; + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + notifyService.notifyNewReport(report, dealerName); + } + }); } @Override diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 6f858ed..bd0810d 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -65,6 +65,14 @@ crm: ttl-days: 90 # 保护期天数 allow-overlap: false # 是否允许重叠报备(生产环境必须为 false) +# pushplus 消息推送配置(经销商提交报备时通知管理员微信) +pushplus: + enabled: true # 配好 token 后改为 true + token: f832934c1b224db3a49306bc8214dc85 # pushplus 用户 token(官网登录后获取) + topic: ops # 群组编码(一对多消息中创建群组得到,管理员扫码订阅) + url: https://www.pushplus.plus/send + approval-url: https://crm.itrackvr.com/ # 推送消息中提示前往审批的系统地址 + # Swagger 配置 springfox: documentation: