feat/multi-channel-framework [AC-INIT]合并功能代码 #12

Merged
MerCry merged 37 commits from feat/multi-channel-framework into main 2026-02-24 03:55:00 +00:00
2 changed files with 168 additions and 0 deletions
Showing only changes of commit d3b696d9bb - Show all commits

View File

@ -0,0 +1,56 @@
package com.wecom.robot.util;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
@RequiredArgsConstructor
public class IdempotentHelper {
private static final String KEY_PREFIX = "idempotent:";
private static final long DEFAULT_TTL_HOURS = 1;
private final StringRedisTemplate redisTemplate;
public boolean processMessageIdempotent(String channelMessageId, Runnable processor) {
String key = KEY_PREFIX + channelMessageId;
Boolean absent = redisTemplate.opsForValue()
.setIfAbsent(key, "1", DEFAULT_TTL_HOURS, TimeUnit.HOURS);
if (Boolean.TRUE.equals(absent)) {
processor.run();
return true;
}
log.info("[AC-MCA-11-IDEMPOTENT] 重复消息,跳过处理: channelMessageId={}", channelMessageId);
return false;
}
public boolean checkAndSet(String channelMessageId) {
String key = KEY_PREFIX + channelMessageId;
Boolean absent = redisTemplate.opsForValue()
.setIfAbsent(key, "1", DEFAULT_TTL_HOURS, TimeUnit.HOURS);
if (Boolean.TRUE.equals(absent)) {
return true;
}
log.info("[AC-MCA-11-IDEMPOTENT] 重复消息检测: channelMessageId={}", channelMessageId);
return false;
}
public boolean exists(String channelMessageId) {
String key = KEY_PREFIX + channelMessageId;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public void remove(String channelMessageId) {
String key = KEY_PREFIX + channelMessageId;
redisTemplate.delete(key);
}
}

View File

@ -0,0 +1,112 @@
package com.wecom.robot.util;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class IdempotentHelperTest {
@Mock
private StringRedisTemplate redisTemplate;
@Mock
private ValueOperations<String, String> valueOperations;
private IdempotentHelper idempotentHelper;
@BeforeEach
void setUp() {
lenient().when(redisTemplate.opsForValue()).thenReturn(valueOperations);
idempotentHelper = new IdempotentHelper(redisTemplate);
}
@Test
void testProcessMessageIdempotent_FirstTime_ShouldProcess() {
String messageId = "msg-123";
when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any(TimeUnit.class)))
.thenReturn(true);
boolean[] processed = {false};
boolean result = idempotentHelper.processMessageIdempotent(messageId, () -> processed[0] = true);
assertTrue(result);
assertTrue(processed[0]);
verify(valueOperations).setIfAbsent(eq("idempotent:msg-123"), eq("1"), eq(1L), eq(TimeUnit.HOURS));
}
@Test
void testProcessMessageIdempotent_Duplicate_ShouldSkip() {
String messageId = "msg-456";
when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any(TimeUnit.class)))
.thenReturn(false);
boolean[] processed = {false};
boolean result = idempotentHelper.processMessageIdempotent(messageId, () -> processed[0] = true);
assertFalse(result);
assertFalse(processed[0]);
}
@Test
void testCheckAndSet_FirstTime_ShouldReturnTrue() {
String messageId = "msg-789";
when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any(TimeUnit.class)))
.thenReturn(true);
boolean result = idempotentHelper.checkAndSet(messageId);
assertTrue(result);
verify(valueOperations).setIfAbsent(eq("idempotent:msg-789"), eq("1"), eq(1L), eq(TimeUnit.HOURS));
}
@Test
void testCheckAndSet_Duplicate_ShouldReturnFalse() {
String messageId = "msg-duplicate";
when(valueOperations.setIfAbsent(anyString(), anyString(), anyLong(), any(TimeUnit.class)))
.thenReturn(false);
boolean result = idempotentHelper.checkAndSet(messageId);
assertFalse(result);
}
@Test
void testExists_KeyExists_ShouldReturnTrue() {
String messageId = "msg-exists";
when(redisTemplate.hasKey("idempotent:msg-exists")).thenReturn(true);
boolean result = idempotentHelper.exists(messageId);
assertTrue(result);
}
@Test
void testExists_KeyNotExists_ShouldReturnFalse() {
String messageId = "msg-notexists";
when(redisTemplate.hasKey("idempotent:msg-notexists")).thenReturn(false);
boolean result = idempotentHelper.exists(messageId);
assertFalse(result);
}
@Test
void testRemove_ShouldDeleteKey() {
String messageId = "msg-remove";
idempotentHelper.remove(messageId);
verify(redisTemplate).delete("idempotent:msg-remove");
}
}