feat(filter): 添加应用版本过滤器并优化异步任务配置

- 新增AppVersionFilter用于验证客户端版本号
- 添加MdcTaskDecorator确保异步任务中的MDC上下文传递
- 在多个模块的TaskSchedulerConfig中配置MDC装饰器
- 修复Redis键值格式统一使用"-uid-"分隔符
- 调整TicketAddRequest中type字段默认值为0
- 优化TraceIdFilter执行顺序为最高优先级
- 将UniPushService的send方法改为异步执行并返回CompletableFuture
This commit is contained in:
曹鹏飞 2026-01-12 17:23:32 +08:00
parent 3a0facbd9e
commit 7db9d6ef8e
11 changed files with 168 additions and 16 deletions

View File

@ -1,5 +1,6 @@
package com.nflg.mobilebroken.admin.config;
import com.nflg.mobilebroken.starter.decorator.MdcTaskDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
@ -34,6 +35,19 @@ public class TaskSchedulerConfig {
executor.setThreadNamePrefix("sse-send-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
executor.setTaskDecorator(new MdcTaskDecorator());
return executor;
}
@Bean(name = "httpExecutor")
public Executor httpExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("http-async-");
executor.initialize();
executor.setTaskDecorator(new MdcTaskDecorator());
return executor;
}
}

View File

@ -1387,7 +1387,6 @@ public class TicketController extends ControllerBase {
*/
@GetMapping("call")
public ApiResult<Void> call(@Valid @RequestParam @NotNull Integer ticketId) {
Ticket ticket = ticketService.getById(ticketId);
VUtils.trueThrowBusinessError(Objects.isNull(ticket)).throwMessage("工单不存在");
VUtils.trueThrowBusinessError(!Objects.equals(ticket.getState(), TicketState.Processing.getState()))
@ -1446,7 +1445,7 @@ public class TicketController extends ControllerBase {
ssePushService.sendTicketCallToAdmin(adminUser, receiveUserId, Long.valueOf(ticketId));
}
ticketEventPublisher.publishTicketCallBeginEvent(ticketId, adminUser.getUserName());
stringRedisTemplate.opsForSet().add(Constant.REDIS_KEY_TICKET_CALL_WAIT + ticketId, receiveUserFrom + "-" + receiveUserId);
stringRedisTemplate.opsForSet().add(Constant.REDIS_KEY_TICKET_CALL_WAIT + ticketId, receiveUserFrom + "-uid-" + receiveUserId);
stringRedisTemplate.expire(Constant.REDIS_KEY_TICKET_CALL_WAIT + ticketId, 1, TimeUnit.MINUTES);
return ApiResult.success();
}
@ -1493,7 +1492,7 @@ public class TicketController extends ControllerBase {
);
ssePushService.sendTicketCallToAdmin(adminUser, userId, request.getTicketId());
// ticketCallJoinService.add(ticketCall.getId(), userId, Constant.FROM_ADMIN);
stringRedisTemplate.opsForSet().add(Constant.REDIS_KEY_TICKET_CALL_WAIT + request.getTicketId(), Constant.FROM_ADMIN + "-" + userId);
stringRedisTemplate.opsForSet().add(Constant.REDIS_KEY_TICKET_CALL_WAIT + request.getTicketId(), Constant.FROM_ADMIN + "-uid-" + userId);
stringRedisTemplate.expire(Constant.REDIS_KEY_TICKET_CALL_WAIT + request.getTicketId(), 1, TimeUnit.MINUTES);
}
}
@ -1561,7 +1560,7 @@ public class TicketController extends ControllerBase {
.setTitle("视频通话结束")
.setTicketId(String.valueOf(request.getTicketId()))
.setTicketType(ticket.getType())
.setUserId(Integer.valueOf(StrUtil.split(uid, "-").get(1)))
.setUserId(Integer.valueOf(StrUtil.split(uid, "-").get(2)))
.setCategory("ticketCallEnd")
.setFrom("admin")
)
@ -1589,7 +1588,7 @@ public class TicketController extends ControllerBase {
)
);
}
stringRedisTemplate.opsForSet().remove(Constant.REDIS_KEY_TICKET_CALL_WAIT + request.getTicketId(), Constant.FROM_ADMIN + "-" + AdminUserUtil.getUserId());
stringRedisTemplate.opsForSet().remove(Constant.REDIS_KEY_TICKET_CALL_WAIT + request.getTicketId(), Constant.FROM_ADMIN + "-uid-" + AdminUserUtil.getUserId());
}
taskScheduler.schedule(() -> {
ticketEventPublisher.publishTicketCallEndEvent(ticket);

View File

@ -0,0 +1,28 @@
package com.nflg.mobilebroken.cfs.config;
import com.nflg.mobilebroken.starter.decorator.MdcTaskDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@EnableAsync
@Configuration
@EnableScheduling
public class TaskSchedulerConfig {
@Bean(name = "httpExecutor")
public Executor httpExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("http-async-");
executor.initialize();
executor.setTaskDecorator(new MdcTaskDecorator());
return executor;
}
}

View File

@ -655,7 +655,7 @@ public class TicketController extends ControllerBase {
ssePushService.sendTicketCallToAdmin(appUser, handlerUserId, ticketId);
// ticketCallService.add(ticketId, AppUserUtil.getUserId(),Constant.FROM_APP, handlerUserId, Constant.FROM_ADMIN);
ticketEventPublisher.publishTicketCallBeginEvent(ticketId, appUser.getName());
stringRedisTemplate.opsForSet().add(Constant.REDIS_KEY_TICKET_CALL_WAIT + ticketId, Constant.FROM_ADMIN + "-" + handlerUserId);
stringRedisTemplate.opsForSet().add(Constant.REDIS_KEY_TICKET_CALL_WAIT + ticketId, Constant.FROM_ADMIN + "-uid-" + handlerUserId);
stringRedisTemplate.expire(Constant.REDIS_KEY_TICKET_CALL_WAIT + ticketId, 1, TimeUnit.MINUTES);
return ApiResult.success();
}
@ -716,7 +716,7 @@ public class TicketController extends ControllerBase {
.setTitle("视频通话结束")
.setTicketId(String.valueOf(request.getTicketId()))
.setTicketType(ticket.getType())
.setUserId(Integer.valueOf(StrUtil.split(uid, "-").get(1)))
.setUserId(Integer.valueOf(StrUtil.split(uid, "-").get(2)))
.setCategory("ticketCallEnd")
.setFrom("app")
)
@ -742,7 +742,7 @@ public class TicketController extends ControllerBase {
)
)
);
stringRedisTemplate.opsForSet().remove(Constant.REDIS_KEY_TICKET_CALL_WAIT + request.getTicketId(), Constant.FROM_APP + "-" + AppUserUtil.getUserId());
stringRedisTemplate.opsForSet().remove(Constant.REDIS_KEY_TICKET_CALL_WAIT + request.getTicketId(), Constant.FROM_APP + "-uid-" + AppUserUtil.getUserId());
}
taskScheduler.schedule(() -> {
ticketEventPublisher.publishTicketCallEndEvent(ticket);

View File

@ -48,8 +48,7 @@ public class TicketAddRequest {
/**
* 类型0移动破1工服
*/
@NotNull
private Integer type;
private Integer type = 0;
/**
* 紧急程度0非紧急1普通2紧急

View File

@ -1,5 +1,6 @@
package com.nflg.mobilebroken.gongfu.config;
import com.nflg.mobilebroken.starter.decorator.MdcTaskDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
@ -34,6 +35,19 @@ public class TaskSchedulerConfig {
executor.setThreadNamePrefix("sse-send-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
executor.setTaskDecorator(new MdcTaskDecorator());
return executor;
}
@Bean(name = "httpExecutor")
public Executor httpExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("http-async-");
executor.initialize();
executor.setTaskDecorator(new MdcTaskDecorator());
return executor;
}
}

View File

@ -1374,7 +1374,7 @@ public class TicketController extends ControllerBase {
ssePushService.sendTicketCallToAdmin(adminUser, receiveUserId, ticketId);
}
ticketEventPublisher.publishTicketCallBeginEvent(ticketId, adminUser.getUserName());
stringRedisTemplate.opsForSet().add(Constant.REDIS_KEY_TICKET_CALL_WAIT + ticketId, receiveUserFrom + "-" + receiveUserId);
stringRedisTemplate.opsForSet().add(Constant.REDIS_KEY_TICKET_CALL_WAIT + ticketId, receiveUserFrom + "-uid-" + receiveUserId);
stringRedisTemplate.expire(Constant.REDIS_KEY_TICKET_CALL_WAIT + ticketId, 1, TimeUnit.MINUTES);
return ApiResult.success();
}
@ -1421,7 +1421,7 @@ public class TicketController extends ControllerBase {
);
ssePushService.sendTicketCallToAdmin(adminUser, userId, request.getTicketId());
// ticketCallJoinService.add(ticketCall.getId(), userId, Constant.FROM_ADMIN);
stringRedisTemplate.opsForSet().add(Constant.REDIS_KEY_TICKET_CALL_WAIT + request.getTicketId(), Constant.FROM_ADMIN + "-" + userId);
stringRedisTemplate.opsForSet().add(Constant.REDIS_KEY_TICKET_CALL_WAIT + request.getTicketId(), Constant.FROM_ADMIN + "-uid-" + userId);
stringRedisTemplate.expire(Constant.REDIS_KEY_TICKET_CALL_WAIT + request.getTicketId(), 1, TimeUnit.MINUTES);
}
}
@ -1483,7 +1483,7 @@ public class TicketController extends ControllerBase {
// .setPayload(new UniPushMessageCallPayload()
// .setTitle("视频通话结束")
// .setTicketId(request.getTicketId())
// .setUserId(Integer.valueOf(StrUtil.split(uid, "-").get(1)))
// .setUserId(Integer.valueOf(StrUtil.split(uid, "-").get(2)))
// .setCategory("ticketCallEnd")
// .setFrom("admin")
// )

View File

@ -0,0 +1,30 @@
package com.nflg.mobilebroken.starter.decorator;
import lombok.NonNull;
import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;
import java.util.Map;
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(@NonNull Runnable runnable) {
// 1. 获取主线程父线程当前的 MDC 上下文副本
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
// 2. 将父线程的上下文 设置到子线程中
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
// 3. 执行真正的任务
runnable.run();
} finally {
// 4. 务必清除 MDC避免线程复用时内存泄漏或数据污染
MDC.clear();
}
};
}
}

View File

@ -0,0 +1,63 @@
package com.nflg.mobilebroken.starter.filter;
import cn.hutool.core.comparator.VersionComparator;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nflg.mobilebroken.common.constant.STATE;
import com.nflg.mobilebroken.common.pojo.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Slf4j
@Order(0)
@Component
public class AppVersionFilter extends OncePerRequestFilter {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final String MIN_SUPPER_VERSION = "1.0.9";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
String appPlatform = request.getHeader("App-Platform");
response.setStatus(HttpServletResponse.SC_OK);
if (StrUtil.isBlank(appPlatform)) {
log.error("请求头中未找到App-Platform");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
out(response, ApiResult.error(STATE.ServiceConnectRefused, "请更新版本!"));
} else {
if (appPlatform.startsWith("pc")) {
filterChain.doFilter(request, response);
} else {
String appVersion = request.getHeader("App-Version");
if (StrUtil.isBlank(appVersion)) {
log.error("请求头中未找到App-Version");
out(response, ApiResult.error(STATE.ServiceConnectRefused, "请更新版本!"));
} else if (VersionComparator.INSTANCE.compare(appVersion, MIN_SUPPER_VERSION) < 0) {
out(response, ApiResult.error(STATE.ServiceConnectRefused, "版本太低,请更新版本!"));
} else {
filterChain.doFilter(request, response);
}
}
}
}
private void out(HttpServletResponse response, ApiResult result) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(result));
writer.flush();
}
}

View File

@ -9,6 +9,7 @@ import com.nflg.mobilebroken.common.util.SaTokenAdminUtil;
import com.nflg.mobilebroken.common.util.SaTokenAppUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
@ -26,6 +27,7 @@ import java.util.Arrays;
import java.util.List;
@Slf4j
@Order(1)
@Component
public class TraceIdFilter extends OncePerRequestFilter {
@ -48,13 +50,11 @@ public class TraceIdFilter extends OncePerRequestFilter {
// 存入MDC和响应头
MDC.put(Constant.TRACE_ID, traceId);
responseWrapper.addHeader(TRACE_ID_HEADER, traceId);
filterChain.doFilter(requestWrapper, responseWrapper);
} finally {
logRequest(requestWrapper);
logResponse(responseWrapper);
responseWrapper.copyBodyToResponse();
// 请求结束时清除MDC
MDC.remove(Constant.TRACE_ID);
}

View File

@ -11,9 +11,12 @@ import com.nflg.mobilebroken.common.util.AppUserUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Component
public class UniPushService {
@ -21,11 +24,13 @@ public class UniPushService {
@Value("${uniapp.cloud.push.url}")
private String url;
public void send(UniPushMessage message) {
@Async("httpExecutor")
public CompletableFuture<Void> send(UniPushMessage message) {
log.info("发送uniapp消息{}", JSONUtil.toJsonStr(message));
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.postForEntity(url, message, String.class);
log.info("发送uniapp消息结果{}", response.getBody());
return CompletableFuture.completedFuture(null);
}
public void sendTicketMessage(String from, String to, ChatMessageDTO data) {