andy_dev_2026 #1
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.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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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.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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user