feat: 新增pushplus微信推送新报备通知功能

实现pushplus微信推送新报备通知的全套功能:
- 新增通知服务接口与实现类
- 配置异步线程池避免阻塞主业务
- 补充application.yml推送配置项
- 添加管理员测试推送API接口
- 在报备创建事务提交后触发通知,确保仅成功提交的报备发送通知
This commit is contained in:
wanglongjie 2026-08-03 16:06:09 +08:00
parent fd89c31640
commit f400a93bdd
6 changed files with 230 additions and 2 deletions

View File

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

View File

@ -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<Void> 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();
}
}

View File

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

View File

@ -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 = "<h3>泊云智销通推送测试</h3><p>收到本消息说明 pushplus 配置正常,经销商提交报备时您将收到通知。</p>";
send("泊云智销通推送测试", content);
}
/**
* 实际调用 pushplus 接口全程容错失败仅记日志
*/
private void send(String title, String content) {
try {
Map<String, Object> 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("<h3>有新的报备申请待审核</h3>");
sb.append("<table border='1' cellspacing='0' cellpadding='6' ")
.append("style='border-collapse:collapse;font-size:14px;'>");
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("</table>");
if (approvalUrl != null && !approvalUrl.trim().isEmpty()) {
sb.append("<p style='margin-top:12px;'>请跳转 <a href='")
.append(approvalUrl)
.append("'>")
.append(approvalUrl)
.append("</a> 去审批处理。</p>");
}
return sb.toString();
}
private void row(StringBuilder sb, String label, String value) {
sb.append("<tr><td style='background:#fafafa;font-weight:bold;'>")
.append(label)
.append("</td><td>")
.append(value)
.append("</td></tr>");
}
private String safe(String s) {
return s == null ? "-" : s;
}
}

View File

@ -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

View File

@ -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: