feat: 新增pushplus微信推送新报备通知功能
实现pushplus微信推送新报备通知的全套功能: - 新增通知服务接口与实现类 - 配置异步线程池避免阻塞主业务 - 补充application.yml推送配置项 - 添加管理员测试推送API接口 - 在报备创建事务提交后触发通知,确保仅成功提交的报备发送通知
This commit is contained in:
parent
fd89c31640
commit
f400a93bdd
34
backend/src/main/java/com/bycrm/config/AsyncConfig.java
Normal file
34
backend/src/main/java/com/bycrm/config/AsyncConfig.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import com.bycrm.common.Result;
|
||||||
import com.bycrm.entity.SystemConfig;
|
import com.bycrm.entity.SystemConfig;
|
||||||
import com.bycrm.entity.User;
|
import com.bycrm.entity.User;
|
||||||
import com.bycrm.mapper.UserMapper;
|
import com.bycrm.mapper.UserMapper;
|
||||||
|
import com.bycrm.service.NotifyService;
|
||||||
import com.bycrm.service.SystemConfigService;
|
import com.bycrm.service.SystemConfigService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
|
@ -23,11 +24,14 @@ public class SystemConfigController {
|
||||||
|
|
||||||
private final SystemConfigService systemConfigService;
|
private final SystemConfigService systemConfigService;
|
||||||
private final UserMapper userMapper;
|
private final UserMapper userMapper;
|
||||||
|
private final NotifyService notifyService;
|
||||||
|
|
||||||
public SystemConfigController(SystemConfigService systemConfigService,
|
public SystemConfigController(SystemConfigService systemConfigService,
|
||||||
UserMapper userMapper) {
|
UserMapper userMapper,
|
||||||
|
NotifyService notifyService) {
|
||||||
this.systemConfigService = systemConfigService;
|
this.systemConfigService = systemConfigService;
|
||||||
this.userMapper = userMapper;
|
this.userMapper = userMapper;
|
||||||
|
this.notifyService = notifyService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -104,4 +108,19 @@ public class SystemConfigController {
|
||||||
systemConfigService.batchUpdateConfigs(configs);
|
systemConfigService.batchUpdateConfigs(configs);
|
||||||
return Result.success();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
backend/src/main/java/com/bycrm/service/NotifyService.java
Normal file
22
backend/src/main/java/com/bycrm/service/NotifyService.java
Normal 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();
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,12 +15,15 @@ import com.bycrm.mapper.DealerMapper;
|
||||||
import com.bycrm.mapper.ReportMapper;
|
import com.bycrm.mapper.ReportMapper;
|
||||||
import com.bycrm.mapper.SchoolMapper;
|
import com.bycrm.mapper.SchoolMapper;
|
||||||
import com.bycrm.mapper.UserMapper;
|
import com.bycrm.mapper.UserMapper;
|
||||||
|
import com.bycrm.service.NotifyService;
|
||||||
import com.bycrm.service.ReportService;
|
import com.bycrm.service.ReportService;
|
||||||
import com.bycrm.service.SystemConfigService;
|
import com.bycrm.service.SystemConfigService;
|
||||||
import com.bycrm.vo.ReportVO;
|
import com.bycrm.vo.ReportVO;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
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.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
@ -39,17 +42,20 @@ public class ReportServiceImpl implements ReportService {
|
||||||
private final UserMapper userMapper;
|
private final UserMapper userMapper;
|
||||||
private final DealerMapper dealerMapper;
|
private final DealerMapper dealerMapper;
|
||||||
private final SystemConfigService systemConfigService;
|
private final SystemConfigService systemConfigService;
|
||||||
|
private final NotifyService notifyService;
|
||||||
|
|
||||||
public ReportServiceImpl(ReportMapper reportMapper,
|
public ReportServiceImpl(ReportMapper reportMapper,
|
||||||
SchoolMapper schoolMapper,
|
SchoolMapper schoolMapper,
|
||||||
UserMapper userMapper,
|
UserMapper userMapper,
|
||||||
DealerMapper dealerMapper,
|
DealerMapper dealerMapper,
|
||||||
SystemConfigService systemConfigService) {
|
SystemConfigService systemConfigService,
|
||||||
|
NotifyService notifyService) {
|
||||||
this.reportMapper = reportMapper;
|
this.reportMapper = reportMapper;
|
||||||
this.schoolMapper = schoolMapper;
|
this.schoolMapper = schoolMapper;
|
||||||
this.userMapper = userMapper;
|
this.userMapper = userMapper;
|
||||||
this.dealerMapper = dealerMapper;
|
this.dealerMapper = dealerMapper;
|
||||||
this.systemConfigService = systemConfigService;
|
this.systemConfigService = systemConfigService;
|
||||||
|
this.notifyService = notifyService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -162,6 +168,16 @@ public class ReportServiceImpl implements ReportService {
|
||||||
report.setUpdatedAt(LocalDateTime.now());
|
report.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
reportMapper.insert(report);
|
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
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,14 @@ crm:
|
||||||
ttl-days: 90 # 保护期天数
|
ttl-days: 90 # 保护期天数
|
||||||
allow-overlap: false # 是否允许重叠报备(生产环境必须为 false)
|
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 配置
|
# Swagger 配置
|
||||||
springfox:
|
springfox:
|
||||||
documentation:
|
documentation:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user