feat(send): route by conversation_scene and person_type
This commit is contained in:
@@ -60,6 +60,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -79,6 +80,12 @@ public class SendService {
|
|||||||
private static final int SEND_STATUS_SUCCESS = 2;
|
private static final int SEND_STATUS_SUCCESS = 2;
|
||||||
private static final int SEND_STATUS_FAILED = 3;
|
private static final int SEND_STATUS_FAILED = 3;
|
||||||
|
|
||||||
|
private static final int CONVERSATION_SCENE_INTERNAL = 1;
|
||||||
|
private static final int CONVERSATION_SCENE_EXTERNAL = 2;
|
||||||
|
private static final Set<Integer> ACCOUNT_TYPES_BOT = Set.of(6);
|
||||||
|
private static final Set<Integer> ACCOUNT_TYPES_PYWECHAT = Set.of(3);
|
||||||
|
private static final Set<Integer> ACCOUNT_TYPES_WECOM_AGENT = Set.of(2);
|
||||||
|
|
||||||
private final SendRecordMapper sendRecordMapper;
|
private final SendRecordMapper sendRecordMapper;
|
||||||
private final ConversationMapper conversationMapper;
|
private final ConversationMapper conversationMapper;
|
||||||
private final ChannelAccountMapper accountMapper;
|
private final ChannelAccountMapper accountMapper;
|
||||||
@@ -309,28 +316,47 @@ public class SendService {
|
|||||||
}
|
}
|
||||||
return targets;
|
return targets;
|
||||||
case "tag":
|
case "tag":
|
||||||
Number tagId = (Number) targetValue.get("tag_id");
|
return resolveTagTargetFromValue(tenantCode, targetValue, channelType);
|
||||||
if (tagId == null) {
|
|
||||||
throw new BusinessException("tag 目标缺少 tag_id");
|
|
||||||
}
|
|
||||||
String scope = (String) targetValue.getOrDefault("scope", "all");
|
|
||||||
return resolveTagTarget(tenantCode, tagId.longValue(), channelType, scope);
|
|
||||||
case "rule":
|
case "rule":
|
||||||
return resolveRuleTarget(tenantCode, targetValue, channelType, channelStrategy);
|
case "combined":
|
||||||
|
return resolveCombinedTarget(tenantCode, targetValue, channelType, channelStrategy);
|
||||||
default:
|
default:
|
||||||
throw new BusinessException("不支持的目标类型: " + targetType);
|
throw new BusinessException("不支持的目标类型: " + targetType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ChannelType resolveChannelTypeForScene(Integer scene) {
|
||||||
|
if (scene == null) {
|
||||||
|
throw new BusinessException("会话未设置场景,无法自动路由");
|
||||||
|
}
|
||||||
|
return scene == CONVERSATION_SCENE_INTERNAL ? ChannelType.WECOM : ChannelType.WECHAT_PERSONAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Integer> resolveAccountTypesForScene(Integer scene) {
|
||||||
|
if (scene == null) {
|
||||||
|
throw new BusinessException("会话未设置场景,无法自动路由");
|
||||||
|
}
|
||||||
|
return scene == CONVERSATION_SCENE_INTERNAL ? ACCOUNT_TYPES_BOT : ACCOUNT_TYPES_PYWECHAT;
|
||||||
|
}
|
||||||
|
|
||||||
private List<SendTarget> resolveConversationTarget(String tenantCode, Long conversationId,
|
private List<SendTarget> resolveConversationTarget(String tenantCode, Long conversationId,
|
||||||
ChannelType channelType, Map<String, Object> channelStrategy) {
|
ChannelType ignoredChannelType,
|
||||||
|
Map<String, Object> channelStrategy) {
|
||||||
Conversation conversation = conversationMapper.selectById(conversationId);
|
Conversation conversation = conversationMapper.selectById(conversationId);
|
||||||
if (conversation == null || !tenantCode.equals(conversation.getTenantCode())) {
|
if (conversation == null || !tenantCode.equals(conversation.getTenantCode())) {
|
||||||
throw new BusinessException("会话不存在: " + conversationId);
|
throw new BusinessException("会话不存在: " + conversationId);
|
||||||
}
|
}
|
||||||
Long accountId = selectSendAccount(tenantCode, conversationId, channelType, channelStrategy);
|
|
||||||
|
ChannelType channelType = resolveChannelTypeForScene(conversation.getConversationScene());
|
||||||
|
Set<Integer> accountTypes = resolveAccountTypesForScene(conversation.getConversationScene());
|
||||||
|
|
||||||
|
Long accountId = selectSendAccount(tenantCode, conversationId, channelType, accountTypes, channelStrategy);
|
||||||
if (accountId == null) {
|
if (accountId == null) {
|
||||||
accountId = conversation.getOwnerAccountId();
|
accountId = conversation.getOwnerAccountId();
|
||||||
|
ChannelAccount ownerAccount = accountMapper.selectById(accountId);
|
||||||
|
if (ownerAccount == null || !accountTypes.contains(ownerAccount.getAccountType())) {
|
||||||
|
accountId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (accountId == null) {
|
if (accountId == null) {
|
||||||
throw new BusinessException("会话未配置发送账号: " + conversationId);
|
throw new BusinessException("会话未配置发送账号: " + conversationId);
|
||||||
@@ -349,17 +375,22 @@ public class SendService {
|
|||||||
return Collections.singletonList(target);
|
return Collections.singletonList(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SendTarget> resolvePersonTarget(String tenantCode, Long personId, ChannelType channelType) {
|
private List<SendTarget> resolvePersonTarget(String tenantCode, Long personId, ChannelType ignoredChannelType) {
|
||||||
|
Person person = personMapper.selectById(personId);
|
||||||
|
if (person == null || !tenantCode.equals(person.getTenantCode())) {
|
||||||
|
throw new BusinessException("人员不存在: " + personId);
|
||||||
|
}
|
||||||
ChannelIdentity identity = channelIdentityMapper.selectOne(
|
ChannelIdentity identity = channelIdentityMapper.selectOne(
|
||||||
new LambdaQueryWrapper<ChannelIdentity>()
|
new LambdaQueryWrapper<ChannelIdentity>()
|
||||||
.eq(ChannelIdentity::getTenantCode, tenantCode)
|
.eq(ChannelIdentity::getTenantCode, tenantCode)
|
||||||
.eq(ChannelIdentity::getPersonId, personId)
|
.eq(ChannelIdentity::getPersonId, personId)
|
||||||
.eq(ChannelIdentity::getChannelType, channelType.name().toLowerCase())
|
.eq(ChannelIdentity::getChannelType, ChannelType.WECOM.name().toLowerCase())
|
||||||
.eq(ChannelIdentity::getStatus, STATUS_ENABLED));
|
.eq(ChannelIdentity::getStatus, STATUS_ENABLED));
|
||||||
if (identity == null) {
|
if (identity == null) {
|
||||||
throw new BusinessException("人员未配置渠道身份: person_id=" + personId + ", channel=" + channelType.name().toLowerCase());
|
throw new BusinessException("人员未配置渠道身份: person_id=" + personId);
|
||||||
}
|
}
|
||||||
Long accountId = selectDefaultAccount(tenantCode, channelType);
|
ChannelType channelType = ChannelType.WECOM;
|
||||||
|
Long accountId = selectDefaultAccount(tenantCode, channelType, ACCOUNT_TYPES_WECOM_AGENT);
|
||||||
if (accountId == null) {
|
if (accountId == null) {
|
||||||
throw new BusinessException("租户未配置可用发送账号: channel=" + channelType.name().toLowerCase());
|
throw new BusinessException("租户未配置可用发送账号: channel=" + channelType.name().toLowerCase());
|
||||||
}
|
}
|
||||||
@@ -377,30 +408,132 @@ public class SendService {
|
|||||||
return Collections.singletonList(target);
|
return Collections.singletonList(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<SendTarget> resolveTagTargetFromValue(String tenantCode, Map<String, Object> targetValue,
|
||||||
|
ChannelType channelType) {
|
||||||
|
List<Long> tagIds = extractTagIds(targetValue);
|
||||||
|
if (CollectionUtils.isEmpty(tagIds)) {
|
||||||
|
throw new BusinessException("tag 目标缺少 tag_id 或 tag_ids");
|
||||||
|
}
|
||||||
|
String scope = (String) targetValue.getOrDefault("scope", "all");
|
||||||
|
String innerLogic = (String) targetValue.getOrDefault("inner_logic", "union");
|
||||||
|
return resolveTagTarget(tenantCode, tagIds, channelType, scope, innerLogic);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<Long> extractTagIds(Map<String, Object> targetValue) {
|
||||||
|
List<Long> tagIds = new ArrayList<>();
|
||||||
|
Object tagIdsObj = targetValue.get("tag_ids");
|
||||||
|
if (tagIdsObj instanceof List) {
|
||||||
|
for (Object item : (List<?>) tagIdsObj) {
|
||||||
|
Long id = toLong(item);
|
||||||
|
if (id != null) {
|
||||||
|
tagIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object singleTagId = targetValue.get("tag_id");
|
||||||
|
if (singleTagId != null) {
|
||||||
|
Long id = toLong(singleTagId);
|
||||||
|
if (id != null && !tagIds.contains(id)) {
|
||||||
|
tagIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tagIds;
|
||||||
|
}
|
||||||
|
|
||||||
private List<SendTarget> resolveTagTarget(String tenantCode, Long tagId, ChannelType channelType, String scope) {
|
private List<SendTarget> resolveTagTarget(String tenantCode, Long tagId, ChannelType channelType, String scope) {
|
||||||
|
return resolveTagTarget(tenantCode, Collections.singletonList(tagId), channelType, scope, "union");
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SendTarget> resolveTagTarget(String tenantCode, List<Long> tagIds, ChannelType channelType,
|
||||||
|
String scope, String innerLogic) {
|
||||||
List<SendTarget> targets = new ArrayList<>();
|
List<SendTarget> targets = new ArrayList<>();
|
||||||
|
String channelTypeStr = channelType.name().toLowerCase();
|
||||||
|
boolean union = !"intersection".equals(innerLogic);
|
||||||
|
|
||||||
if ("all".equals(scope) || "conversation".equals(scope)) {
|
if ("all".equals(scope) || "conversation".equals(scope)) {
|
||||||
List<ConversationTagBinding> convBindings = conversationTagBindingMapper.selectList(
|
Set<Long> conversationIds = union ? new HashSet<>() : null;
|
||||||
new LambdaQueryWrapper<ConversationTagBinding>()
|
if (!union) {
|
||||||
.eq(ConversationTagBinding::getTenantCode, tenantCode)
|
conversationIds = selectConversationIdsByAllTags(tenantCode, tagIds);
|
||||||
.eq(ConversationTagBinding::getTagId, tagId));
|
} else {
|
||||||
for (ConversationTagBinding binding : convBindings) {
|
for (Long tagId : tagIds) {
|
||||||
targets.addAll(resolveConversationTarget(tenantCode, binding.getConversationId(), channelType,
|
conversationIds.addAll(selectConversationIdsByTag(tenantCode, tagId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Long conversationId : conversationIds) {
|
||||||
|
Conversation conversation = conversationMapper.selectById(conversationId);
|
||||||
|
if (conversation == null || !tenantCode.equals(conversation.getTenantCode())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!channelTypeStr.equals(conversation.getChannelType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
targets.addAll(resolveConversationTarget(tenantCode, conversationId, channelType,
|
||||||
Collections.singletonMap("strategy", "primary")));
|
Collections.singletonMap("strategy", "primary")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ("all".equals(scope) || "person".equals(scope)) {
|
if ("all".equals(scope) || "person".equals(scope)) {
|
||||||
List<PersonTagBinding> personBindings = personTagBindingMapper.selectList(
|
Set<Long> personIds = union ? new HashSet<>() : null;
|
||||||
new LambdaQueryWrapper<PersonTagBinding>()
|
if (!union) {
|
||||||
.eq(PersonTagBinding::getTenantCode, tenantCode)
|
personIds = selectPersonIdsByAllTags(tenantCode, tagIds);
|
||||||
.eq(PersonTagBinding::getTagId, tagId));
|
} else {
|
||||||
for (PersonTagBinding binding : personBindings) {
|
for (Long tagId : tagIds) {
|
||||||
targets.addAll(resolvePersonTarget(tenantCode, binding.getPersonId(), channelType));
|
personIds.addAll(selectPersonIdsByTag(tenantCode, tagId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Long personId : personIds) {
|
||||||
|
targets.addAll(resolvePersonTarget(tenantCode, personId, channelType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return targets;
|
return targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Set<Long> selectConversationIdsByTag(String tenantCode, Long tagId) {
|
||||||
|
return conversationTagBindingMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<ConversationTagBinding>()
|
||||||
|
.eq(ConversationTagBinding::getTenantCode, tenantCode)
|
||||||
|
.eq(ConversationTagBinding::getTagId, tagId))
|
||||||
|
.stream()
|
||||||
|
.map(ConversationTagBinding::getConversationId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Long> selectConversationIdsByAllTags(String tenantCode, List<Long> tagIds) {
|
||||||
|
Set<Long> ids = null;
|
||||||
|
for (Long tagId : tagIds) {
|
||||||
|
Set<Long> current = selectConversationIdsByTag(tenantCode, tagId);
|
||||||
|
if (ids == null) {
|
||||||
|
ids = new HashSet<>(current);
|
||||||
|
} else {
|
||||||
|
ids.retainAll(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids == null ? Collections.emptySet() : ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Long> selectPersonIdsByTag(String tenantCode, Long tagId) {
|
||||||
|
return personTagBindingMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<PersonTagBinding>()
|
||||||
|
.eq(PersonTagBinding::getTenantCode, tenantCode)
|
||||||
|
.eq(PersonTagBinding::getTagId, tagId))
|
||||||
|
.stream()
|
||||||
|
.map(PersonTagBinding::getPersonId)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Long> selectPersonIdsByAllTags(String tenantCode, List<Long> tagIds) {
|
||||||
|
Set<Long> ids = null;
|
||||||
|
for (Long tagId : tagIds) {
|
||||||
|
Set<Long> current = selectPersonIdsByTag(tenantCode, tagId);
|
||||||
|
if (ids == null) {
|
||||||
|
ids = new HashSet<>(current);
|
||||||
|
} else {
|
||||||
|
ids.retainAll(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids == null ? Collections.emptySet() : ids;
|
||||||
|
}
|
||||||
|
|
||||||
private List<SendTarget> resolveRuleTarget(String tenantCode, Map<String, Object> targetValue,
|
private List<SendTarget> resolveRuleTarget(String tenantCode, Map<String, Object> targetValue,
|
||||||
ChannelType channelType, Map<String, Object> channelStrategy) {
|
ChannelType channelType, Map<String, Object> channelStrategy) {
|
||||||
RuleTargetValue ruleTarget = parseRuleTargetValue(targetValue);
|
RuleTargetValue ruleTarget = parseRuleTargetValue(targetValue);
|
||||||
@@ -421,6 +554,125 @@ public class SendService {
|
|||||||
return targets;
|
return targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private List<SendTarget> resolveCombinedTarget(String tenantCode, Map<String, Object> targetValue,
|
||||||
|
ChannelType channelType, Map<String, Object> channelStrategy) {
|
||||||
|
if (targetValue == null) {
|
||||||
|
throw new BusinessException("combined 目标缺少 conditions");
|
||||||
|
}
|
||||||
|
Object conditionsObj = targetValue.get("conditions");
|
||||||
|
if (!(conditionsObj instanceof List) || ((List<?>) conditionsObj).isEmpty()) {
|
||||||
|
throw new BusinessException("combined 目标缺少 conditions");
|
||||||
|
}
|
||||||
|
String logic = toRuleString(targetValue.getOrDefault("logic", "and"));
|
||||||
|
if (!"and".equals(logic) && !"or".equals(logic)) {
|
||||||
|
throw new BusinessException("combined 逻辑仅支持 and 或 or");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<SendTarget>> groups = new ArrayList<>();
|
||||||
|
for (Object item : (List<?>) conditionsObj) {
|
||||||
|
if (!(item instanceof Map)) {
|
||||||
|
throw new BusinessException("combined 条件格式错误");
|
||||||
|
}
|
||||||
|
Map<String, Object> condition = (Map<String, Object>) item;
|
||||||
|
List<SendTarget> group;
|
||||||
|
// 支持嵌套条件组:{ logic, conditions }
|
||||||
|
if (condition.containsKey("logic") && condition.containsKey("conditions")) {
|
||||||
|
group = resolveCombinedTarget(tenantCode, condition, channelType, channelStrategy);
|
||||||
|
} else {
|
||||||
|
String type = toRuleString(condition.get("type"));
|
||||||
|
Object valueObj = condition.get("value");
|
||||||
|
if (!StringUtils.hasText(type) || !(valueObj instanceof Map)) {
|
||||||
|
throw new BusinessException("combined 条件缺少 type 或 value");
|
||||||
|
}
|
||||||
|
Map<String, Object> value = (Map<String, Object>) valueObj;
|
||||||
|
group = resolveCombinedCondition(tenantCode, type, value, channelType, channelStrategy);
|
||||||
|
}
|
||||||
|
if (!group.isEmpty()) {
|
||||||
|
groups.add(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groups.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
if ("and".equals(logic)) {
|
||||||
|
return intersectTargets(groups);
|
||||||
|
}
|
||||||
|
return unionTargets(groups);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SendTarget> resolveCombinedCondition(String tenantCode, String type, Map<String, Object> value,
|
||||||
|
ChannelType channelType, Map<String, Object> channelStrategy) {
|
||||||
|
switch (type) {
|
||||||
|
case "tag":
|
||||||
|
return resolveTagTargetFromValue(tenantCode, value, channelType);
|
||||||
|
case "conversation":
|
||||||
|
Number convId = (Number) value.get("conversation_id");
|
||||||
|
if (convId == null) {
|
||||||
|
throw new BusinessException("combined conversation 条件缺少 conversation_id");
|
||||||
|
}
|
||||||
|
return resolveConversationTarget(tenantCode, convId.longValue(), channelType, channelStrategy);
|
||||||
|
case "person":
|
||||||
|
Number personId = (Number) value.get("person_id");
|
||||||
|
if (personId == null) {
|
||||||
|
throw new BusinessException("combined person 条件缺少 person_id");
|
||||||
|
}
|
||||||
|
return resolvePersonTarget(tenantCode, personId.longValue(), channelType);
|
||||||
|
default:
|
||||||
|
throw new BusinessException("combined 不支持的条件类型: " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SendTarget> unionTargets(List<List<SendTarget>> groups) {
|
||||||
|
Map<String, SendTarget> merged = new LinkedHashMap<>();
|
||||||
|
for (List<SendTarget> group : groups) {
|
||||||
|
for (SendTarget target : group) {
|
||||||
|
merged.put(targetKey(target), target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(merged.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SendTarget> intersectTargets(List<List<SendTarget>> groups) {
|
||||||
|
List<SendTarget> result = new ArrayList<>();
|
||||||
|
Map<String, SendTarget> firstGroup = new LinkedHashMap<>();
|
||||||
|
for (SendTarget target : groups.get(0)) {
|
||||||
|
firstGroup.put(targetKey(target), target);
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, SendTarget> entry : firstGroup.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
boolean inAll = true;
|
||||||
|
for (int i = 1; i < groups.size(); i++) {
|
||||||
|
boolean found = false;
|
||||||
|
for (SendTarget target : groups.get(i)) {
|
||||||
|
if (key.equals(targetKey(target))) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
inAll = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inAll) {
|
||||||
|
result.add(entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String targetKey(SendTarget target) {
|
||||||
|
if (target.getPersonId() != null) {
|
||||||
|
return "P:" + target.getPersonId() + ":" + target.getChannelType();
|
||||||
|
}
|
||||||
|
if (target.getConversationId() != null) {
|
||||||
|
return "C:" + target.getConversationId() + ":" + target.getChannelType();
|
||||||
|
}
|
||||||
|
return "U:" + System.identityHashCode(target);
|
||||||
|
}
|
||||||
|
|
||||||
private RuleTargetValue parseRuleTargetValue(Map<String, Object> targetValue) {
|
private RuleTargetValue parseRuleTargetValue(Map<String, Object> targetValue) {
|
||||||
if (targetValue == null) {
|
if (targetValue == null) {
|
||||||
throw new BusinessException("rule 目标缺少 rules");
|
throw new BusinessException("rule 目标缺少 rules");
|
||||||
@@ -687,18 +939,37 @@ public class SendService {
|
|||||||
|
|
||||||
private Long selectSendAccount(String tenantCode, Long conversationId, ChannelType channelType,
|
private Long selectSendAccount(String tenantCode, Long conversationId, ChannelType channelType,
|
||||||
Map<String, Object> channelStrategy) {
|
Map<String, Object> channelStrategy) {
|
||||||
|
return selectSendAccount(tenantCode, conversationId, channelType, null, channelStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long selectSendAccount(String tenantCode, Long conversationId, ChannelType channelType,
|
||||||
|
Set<Integer> accountTypes, Map<String, Object> channelStrategy) {
|
||||||
List<Long> accounts = accountSelector.selectSendAccounts(tenantCode, conversationId, channelType,
|
List<Long> accounts = accountSelector.selectSendAccounts(tenantCode, conversationId, channelType,
|
||||||
extractStrategy(channelStrategy));
|
accountTypes, extractStrategy(channelStrategy));
|
||||||
return CollectionUtils.isEmpty(accounts) ? null : accounts.get(0);
|
return CollectionUtils.isEmpty(accounts) ? null : accounts.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long selectDefaultAccount(String tenantCode, ChannelType channelType) {
|
private Long selectDefaultAccount(String tenantCode, ChannelType channelType) {
|
||||||
|
return selectDefaultAccount(tenantCode, channelType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long selectDefaultAccount(String tenantCode, ChannelType channelType, Set<Integer> accountTypes) {
|
||||||
List<ChannelAccount> accounts = accountMapper.selectList(
|
List<ChannelAccount> accounts = accountMapper.selectList(
|
||||||
new LambdaQueryWrapper<ChannelAccount>()
|
new LambdaQueryWrapper<ChannelAccount>()
|
||||||
.eq(ChannelAccount::getTenantCode, tenantCode)
|
.eq(ChannelAccount::getTenantCode, tenantCode)
|
||||||
.eq(ChannelAccount::getChannelType, channelType.name().toLowerCase())
|
.eq(ChannelAccount::getChannelType, channelType.name().toLowerCase())
|
||||||
.eq(ChannelAccount::getStatus, STATUS_ENABLED));
|
.eq(ChannelAccount::getStatus, STATUS_ENABLED));
|
||||||
return CollectionUtils.isEmpty(accounts) ? null : accounts.get(0).getId();
|
if (CollectionUtils.isEmpty(accounts)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (accountTypes != null) {
|
||||||
|
return accounts.stream()
|
||||||
|
.filter(a -> accountTypes.contains(a.getAccountType()))
|
||||||
|
.findFirst()
|
||||||
|
.map(ChannelAccount::getId)
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
return accounts.get(0).getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractChannelType(Map<String, Object> channelStrategy) {
|
private String extractChannelType(Map<String, Object> channelStrategy) {
|
||||||
@@ -822,7 +1093,16 @@ public class SendService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Long> candidateAccountIds = selectSendAccountCandidates(record, channelType);
|
Set<Integer> accountTypes;
|
||||||
|
if (record.getConversationId() != null) {
|
||||||
|
Conversation conversation = conversationMapper.selectById(record.getConversationId());
|
||||||
|
accountTypes = resolveAccountTypesForScene(conversation != null ? conversation.getConversationScene() : null);
|
||||||
|
} else if (record.getPersonId() != null) {
|
||||||
|
accountTypes = ACCOUNT_TYPES_WECOM_AGENT;
|
||||||
|
} else {
|
||||||
|
accountTypes = null;
|
||||||
|
}
|
||||||
|
List<Long> candidateAccountIds = selectSendAccountCandidates(record, channelType, accountTypes);
|
||||||
if (candidateAccountIds.isEmpty()) {
|
if (candidateAccountIds.isEmpty()) {
|
||||||
record.setSendStatus(SEND_STATUS_FAILED);
|
record.setSendStatus(SEND_STATUS_FAILED);
|
||||||
record.setChannelResult(JsonUtils.toJson(Collections.singletonMap("error", "no_healthy_account")));
|
record.setChannelResult(JsonUtils.toJson(Collections.singletonMap("error", "no_healthy_account")));
|
||||||
@@ -865,12 +1145,12 @@ public class SendService {
|
|||||||
sendRecordMapper.updateById(record);
|
sendRecordMapper.updateById(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Long> selectSendAccountCandidates(SendRecord record, ChannelType channelType) {
|
private List<Long> selectSendAccountCandidates(SendRecord record, ChannelType channelType, Set<Integer> accountTypes) {
|
||||||
if (record.getConversationId() == null) {
|
if (record.getConversationId() == null) {
|
||||||
return record.getAccountId() != null ? Collections.singletonList(record.getAccountId()) : Collections.emptyList();
|
return record.getAccountId() != null ? Collections.singletonList(record.getAccountId()) : Collections.emptyList();
|
||||||
}
|
}
|
||||||
String preferredStrategy = extractStrategy(parseChannelStrategy(record.getChannelStrategy()));
|
String preferredStrategy = extractStrategy(parseChannelStrategy(record.getChannelStrategy()));
|
||||||
return accountSelector.selectSendAccounts(record.getTenantCode(), record.getConversationId(), channelType, preferredStrategy);
|
return accountSelector.selectSendAccounts(record.getTenantCode(), record.getConversationId(), channelType, accountTypes, preferredStrategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> parseChannelStrategy(String strategyJson) {
|
private Map<String, Object> parseChannelStrategy(String strategyJson) {
|
||||||
@@ -890,14 +1170,17 @@ public class SendService {
|
|||||||
|
|
||||||
private SendMessageResult doSendAttempt(SendRecord record, ChannelAdapter adapter, ChannelAccount account) {
|
private SendMessageResult doSendAttempt(SendRecord record, ChannelAdapter adapter, ChannelAccount account) {
|
||||||
SendMessageRequest messageRequest = new SendMessageRequest();
|
SendMessageRequest messageRequest = new SendMessageRequest();
|
||||||
messageRequest.setConversationId(record.getConversationId() != null ? String.valueOf(record.getConversationId()) : null);
|
messageRequest.setConversationId(resolveChannelConversationId(record));
|
||||||
messageRequest.setReceiverId(record.getPersonId() != null ? String.valueOf(record.getPersonId()) : null);
|
messageRequest.setReceiverId(record.getPersonId() != null ? String.valueOf(record.getPersonId()) : null);
|
||||||
messageRequest.setContentType(record.getContentType());
|
messageRequest.setContentType(record.getContentType());
|
||||||
messageRequest.setContent(JsonUtils.fromJson(record.getContentBody(), Map.class));
|
messageRequest.setContent(JsonUtils.fromJson(record.getContentBody(), Map.class));
|
||||||
|
|
||||||
Map<String, Object> extra = new HashMap<>();
|
Map<String, Object> extra = new HashMap<>();
|
||||||
if (account != null && StringUtils.hasText(account.getAccountId())) {
|
if (account != null) {
|
||||||
extra.put("account_id", account.getAccountId());
|
extra.put("account_type", account.getAccountType());
|
||||||
|
if (StringUtils.hasText(account.getAccountId())) {
|
||||||
|
extra.put("account_id", account.getAccountId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
extra.put("record_id", record.getId());
|
extra.put("record_id", record.getId());
|
||||||
extra.put("tenant_code", record.getTenantCode());
|
extra.put("tenant_code", record.getTenantCode());
|
||||||
@@ -915,6 +1198,17 @@ public class SendService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String resolveChannelConversationId(SendRecord record) {
|
||||||
|
if (record.getConversationId() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Conversation conversation = conversationMapper.selectById(record.getConversationId());
|
||||||
|
if (conversation != null && StringUtils.hasText(conversation.getChannelConversationId())) {
|
||||||
|
return conversation.getChannelConversationId();
|
||||||
|
}
|
||||||
|
return String.valueOf(record.getConversationId());
|
||||||
|
}
|
||||||
|
|
||||||
private void markAccountUnhealthy(ChannelAccount account) {
|
private void markAccountUnhealthy(ChannelAccount account) {
|
||||||
if (account == null || account.getAccountType() == null || account.getAccountType() != 3) {
|
if (account == null || account.getAccountType() == null || account.getAccountType() != 3) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.sino.mci.channel.common.dto.SendMessageResult;
|
|||||||
import com.sino.mci.channel.common.model.ChannelType;
|
import com.sino.mci.channel.common.model.ChannelType;
|
||||||
import com.sino.mci.channel.common.spi.ChannelAdapter;
|
import com.sino.mci.channel.common.spi.ChannelAdapter;
|
||||||
import com.sino.mci.channel.entity.ChannelAccount;
|
import com.sino.mci.channel.entity.ChannelAccount;
|
||||||
|
import com.sino.mci.channel.entity.Conversation;
|
||||||
import com.sino.mci.channel.repository.ChannelAccountMapper;
|
import com.sino.mci.channel.repository.ChannelAccountMapper;
|
||||||
import com.sino.mci.channel.repository.ConversationMapper;
|
import com.sino.mci.channel.repository.ConversationMapper;
|
||||||
import com.sino.mci.channel.repository.ConversationTagBindingMapper;
|
import com.sino.mci.channel.repository.ConversationTagBindingMapper;
|
||||||
@@ -31,6 +32,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
@@ -86,7 +88,8 @@ class SendServiceFailoverTest {
|
|||||||
Long primaryAccountId = 100L;
|
Long primaryAccountId = 100L;
|
||||||
Long backupAccountId = 200L;
|
Long backupAccountId = 200L;
|
||||||
|
|
||||||
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECHAT_PERSONAL, "primary"))
|
when(conversationMapper.selectById(1L)).thenReturn(createExternalConversation());
|
||||||
|
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECHAT_PERSONAL, Set.of(3), "primary"))
|
||||||
.thenReturn(Arrays.asList(primaryAccountId, backupAccountId));
|
.thenReturn(Arrays.asList(primaryAccountId, backupAccountId));
|
||||||
when(accountMapper.selectById(primaryAccountId)).thenReturn(createPywechatAccount(primaryAccountId, "wxid-primary"));
|
when(accountMapper.selectById(primaryAccountId)).thenReturn(createPywechatAccount(primaryAccountId, "wxid-primary"));
|
||||||
when(accountMapper.selectById(backupAccountId)).thenReturn(createPywechatAccount(backupAccountId, "wxid-backup"));
|
when(accountMapper.selectById(backupAccountId)).thenReturn(createPywechatAccount(backupAccountId, "wxid-backup"));
|
||||||
@@ -110,7 +113,9 @@ class SendServiceFailoverTest {
|
|||||||
verify(adapter, times(2)).sendMessage(requestCaptor.capture());
|
verify(adapter, times(2)).sendMessage(requestCaptor.capture());
|
||||||
List<SendMessageRequest> requests = requestCaptor.getAllValues();
|
List<SendMessageRequest> requests = requestCaptor.getAllValues();
|
||||||
assertEquals("wxid-primary", requests.get(0).getExtra().get("account_id"));
|
assertEquals("wxid-primary", requests.get(0).getExtra().get("account_id"));
|
||||||
|
assertEquals(Integer.valueOf(3), requests.get(0).getExtra().get("account_type"));
|
||||||
assertEquals("wxid-backup", requests.get(1).getExtra().get("account_id"));
|
assertEquals("wxid-backup", requests.get(1).getExtra().get("account_id"));
|
||||||
|
assertEquals(Integer.valueOf(3), requests.get(1).getExtra().get("account_type"));
|
||||||
|
|
||||||
ArgumentCaptor<ChannelAccount> accountCaptor = ArgumentCaptor.forClass(ChannelAccount.class);
|
ArgumentCaptor<ChannelAccount> accountCaptor = ArgumentCaptor.forClass(ChannelAccount.class);
|
||||||
verify(accountMapper, times(1)).updateById(accountCaptor.capture());
|
verify(accountMapper, times(1)).updateById(accountCaptor.capture());
|
||||||
@@ -125,7 +130,8 @@ class SendServiceFailoverTest {
|
|||||||
SendRecord record = createRecord();
|
SendRecord record = createRecord();
|
||||||
Long accountId = 100L;
|
Long accountId = 100L;
|
||||||
|
|
||||||
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECHAT_PERSONAL, "primary"))
|
when(conversationMapper.selectById(1L)).thenReturn(createExternalConversation());
|
||||||
|
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECHAT_PERSONAL, Set.of(3), "primary"))
|
||||||
.thenReturn(Collections.singletonList(accountId));
|
.thenReturn(Collections.singletonList(accountId));
|
||||||
when(accountMapper.selectById(accountId)).thenReturn(createPywechatAccount(accountId, "wxid-primary"));
|
when(accountMapper.selectById(accountId)).thenReturn(createPywechatAccount(accountId, "wxid-primary"));
|
||||||
|
|
||||||
@@ -159,7 +165,8 @@ class SendServiceFailoverTest {
|
|||||||
void dispatchRecordFailsWithNoHealthyAccount() {
|
void dispatchRecordFailsWithNoHealthyAccount() {
|
||||||
SendRecord record = createRecord();
|
SendRecord record = createRecord();
|
||||||
|
|
||||||
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECHAT_PERSONAL, "primary"))
|
when(conversationMapper.selectById(1L)).thenReturn(createExternalConversation());
|
||||||
|
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECHAT_PERSONAL, Set.of(3), "primary"))
|
||||||
.thenReturn(Collections.emptyList());
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
sendService.dispatchRecord(record);
|
sendService.dispatchRecord(record);
|
||||||
@@ -187,6 +194,15 @@ class SendServiceFailoverTest {
|
|||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Conversation createExternalConversation() {
|
||||||
|
Conversation conversation = new Conversation();
|
||||||
|
conversation.setId(1L);
|
||||||
|
conversation.setTenantCode("tenant");
|
||||||
|
conversation.setConversationScene(2);
|
||||||
|
conversation.setChannelConversationId("conv-1");
|
||||||
|
return conversation;
|
||||||
|
}
|
||||||
|
|
||||||
private ChannelAccount createPywechatAccount(Long id, String accountId) {
|
private ChannelAccount createPywechatAccount(Long id, String accountId) {
|
||||||
ChannelAccount account = new ChannelAccount();
|
ChannelAccount account = new ChannelAccount();
|
||||||
account.setId(id);
|
account.setId(id);
|
||||||
@@ -197,6 +213,7 @@ class SendServiceFailoverTest {
|
|||||||
account.setStatus(1);
|
account.setStatus(1);
|
||||||
account.setOnlineStatus(1);
|
account.setOnlineStatus(1);
|
||||||
account.setRiskLevel(1);
|
account.setRiskLevel(1);
|
||||||
|
account.setLastHeartbeat(java.time.LocalDateTime.now());
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.sino.mci.channel.repository.ConversationTagBindingMapper;
|
|||||||
import com.sino.mci.channel.service.AccountSelector;
|
import com.sino.mci.channel.service.AccountSelector;
|
||||||
import com.sino.mci.crm.repository.ChannelIdentityMapper;
|
import com.sino.mci.crm.repository.ChannelIdentityMapper;
|
||||||
import com.sino.mci.crm.entity.ChannelIdentity;
|
import com.sino.mci.crm.entity.ChannelIdentity;
|
||||||
|
import com.sino.mci.crm.entity.Person;
|
||||||
import com.sino.mci.crm.repository.PersonMapper;
|
import com.sino.mci.crm.repository.PersonMapper;
|
||||||
import com.sino.mci.crm.repository.PersonTagBindingMapper;
|
import com.sino.mci.crm.repository.PersonTagBindingMapper;
|
||||||
import com.sino.mci.crm.repository.TagMapper;
|
import com.sino.mci.crm.repository.TagMapper;
|
||||||
@@ -31,6 +32,7 @@ import java.util.Collections;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
@@ -76,13 +78,13 @@ class SendServiceResolveTargetTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolveConversationTarget_selectsHealthyPrimaryAccount() {
|
void resolveConversationTarget_selectsHealthyPrimaryAccount() {
|
||||||
Conversation conversation = createConversation();
|
Conversation conversation = createConversation(1);
|
||||||
Long primaryAccountId = 10L;
|
Long primaryAccountId = 10L;
|
||||||
|
|
||||||
when(conversationMapper.selectById(1L)).thenReturn(conversation);
|
when(conversationMapper.selectById(1L)).thenReturn(conversation);
|
||||||
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECOM, "primary"))
|
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECOM, Set.of(6), "primary"))
|
||||||
.thenReturn(Collections.singletonList(primaryAccountId));
|
.thenReturn(Collections.singletonList(primaryAccountId));
|
||||||
when(accountMapper.selectById(primaryAccountId)).thenReturn(createAccount(primaryAccountId, "wecom", 1));
|
when(accountMapper.selectById(primaryAccountId)).thenReturn(createWecomBotAccount(primaryAccountId, 1));
|
||||||
|
|
||||||
List<SendTarget> targets = sendService.resolveTestTargets("tenant", createConversationRequest("primary"));
|
List<SendTarget> targets = sendService.resolveTestTargets("tenant", createConversationRequest("primary"));
|
||||||
|
|
||||||
@@ -93,13 +95,13 @@ class SendServiceResolveTargetTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolveConversationTarget_rejectsUnhealthyOwnerAccount() {
|
void resolveConversationTarget_rejectsUnhealthyOwnerAccount() {
|
||||||
Conversation conversation = createConversation();
|
Conversation conversation = createConversation(1);
|
||||||
conversation.setOwnerAccountId(20L);
|
conversation.setOwnerAccountId(20L);
|
||||||
|
|
||||||
when(conversationMapper.selectById(1L)).thenReturn(conversation);
|
when(conversationMapper.selectById(1L)).thenReturn(conversation);
|
||||||
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECOM, "primary"))
|
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECOM, Set.of(6), "primary"))
|
||||||
.thenReturn(Collections.emptyList());
|
.thenReturn(Collections.emptyList());
|
||||||
ChannelAccount disabledAccount = createAccount(20L, "wecom", 0);
|
ChannelAccount disabledAccount = createWecomBotAccount(20L, 0);
|
||||||
when(accountMapper.selectById(20L)).thenReturn(disabledAccount);
|
when(accountMapper.selectById(20L)).thenReturn(disabledAccount);
|
||||||
|
|
||||||
BusinessException ex = assertThrows(BusinessException.class,
|
BusinessException ex = assertThrows(BusinessException.class,
|
||||||
@@ -109,13 +111,13 @@ class SendServiceResolveTargetTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolveConversationTarget_usesAccountReturnedBySelector() {
|
void resolveConversationTarget_usesAccountReturnedBySelector() {
|
||||||
Conversation conversation = createConversation();
|
Conversation conversation = createConversation(1);
|
||||||
Long backupAccountId = 20L;
|
Long backupAccountId = 20L;
|
||||||
|
|
||||||
when(conversationMapper.selectById(1L)).thenReturn(conversation);
|
when(conversationMapper.selectById(1L)).thenReturn(conversation);
|
||||||
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECOM, "primary"))
|
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECOM, Set.of(6), "primary"))
|
||||||
.thenReturn(Collections.singletonList(backupAccountId));
|
.thenReturn(Collections.singletonList(backupAccountId));
|
||||||
when(accountMapper.selectById(backupAccountId)).thenReturn(createAccount(backupAccountId, "wecom", 1));
|
when(accountMapper.selectById(backupAccountId)).thenReturn(createWecomBotAccount(backupAccountId, 1));
|
||||||
|
|
||||||
List<SendTarget> targets = sendService.resolveTestTargets("tenant", createConversationRequest("primary"));
|
List<SendTarget> targets = sendService.resolveTestTargets("tenant", createConversationRequest("primary"));
|
||||||
|
|
||||||
@@ -125,13 +127,13 @@ class SendServiceResolveTargetTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolveConversationTarget_respectsBackupStrategy() {
|
void resolveConversationTarget_respectsBackupStrategy() {
|
||||||
Conversation conversation = createConversation();
|
Conversation conversation = createConversation(1);
|
||||||
Long backupAccountId = 20L;
|
Long backupAccountId = 20L;
|
||||||
|
|
||||||
when(conversationMapper.selectById(1L)).thenReturn(conversation);
|
when(conversationMapper.selectById(1L)).thenReturn(conversation);
|
||||||
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECOM, "backup"))
|
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECOM, Set.of(6), "backup"))
|
||||||
.thenReturn(Collections.singletonList(backupAccountId));
|
.thenReturn(Collections.singletonList(backupAccountId));
|
||||||
when(accountMapper.selectById(backupAccountId)).thenReturn(createAccount(backupAccountId, "wecom", 1));
|
when(accountMapper.selectById(backupAccountId)).thenReturn(createWecomBotAccount(backupAccountId, 1));
|
||||||
|
|
||||||
List<SendTarget> targets = sendService.resolveTestTargets("tenant", createConversationRequest("backup"));
|
List<SendTarget> targets = sendService.resolveTestTargets("tenant", createConversationRequest("backup"));
|
||||||
|
|
||||||
@@ -139,15 +141,45 @@ class SendServiceResolveTargetTest {
|
|||||||
assertEquals(backupAccountId, targets.get(0).getAccountId());
|
assertEquals(backupAccountId, targets.get(0).getAccountId());
|
||||||
|
|
||||||
ArgumentCaptor<String> strategyCaptor = ArgumentCaptor.forClass(String.class);
|
ArgumentCaptor<String> strategyCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
verify(accountSelector).selectSendAccounts(eq("tenant"), eq(1L), eq(ChannelType.WECOM), strategyCaptor.capture());
|
verify(accountSelector).selectSendAccounts(eq("tenant"), eq(1L), eq(ChannelType.WECOM), eq(Set.of(6)), strategyCaptor.capture());
|
||||||
assertEquals("backup", strategyCaptor.getValue());
|
assertEquals("backup", strategyCaptor.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolvePersonTarget_rejectsDisabledAccount() {
|
void resolveConversationTarget_routesExternalSceneToPywechat() {
|
||||||
ChannelIdentity identity = createIdentity();
|
Conversation conversation = createConversation(2);
|
||||||
ChannelAccount account = createAccount(30L, "wecom", 0);
|
Long pywechatAccountId = 30L;
|
||||||
|
|
||||||
|
when(conversationMapper.selectById(1L)).thenReturn(conversation);
|
||||||
|
when(accountSelector.selectSendAccounts("tenant", 1L, ChannelType.WECHAT_PERSONAL, Set.of(3), "primary"))
|
||||||
|
.thenReturn(Collections.singletonList(pywechatAccountId));
|
||||||
|
when(accountMapper.selectById(pywechatAccountId)).thenReturn(createPywechatAccount(pywechatAccountId, 1));
|
||||||
|
|
||||||
|
List<SendTarget> targets = sendService.resolveTestTargets("tenant", createConversationRequest("primary"));
|
||||||
|
|
||||||
|
assertEquals(1, targets.size());
|
||||||
|
assertEquals(pywechatAccountId, targets.get(0).getAccountId());
|
||||||
|
assertEquals(ChannelType.WECHAT_PERSONAL, targets.get(0).getChannelType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveConversationTarget_rejectsNullScene() {
|
||||||
|
Conversation conversation = createConversation(null);
|
||||||
|
|
||||||
|
when(conversationMapper.selectById(1L)).thenReturn(conversation);
|
||||||
|
|
||||||
|
BusinessException ex = assertThrows(BusinessException.class,
|
||||||
|
() -> sendService.resolveTestTargets("tenant", createConversationRequest("primary")));
|
||||||
|
assertEquals("会话未设置场景,无法自动路由", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvePersonTarget_rejectsDisabledAccount() {
|
||||||
|
Person person = createPerson();
|
||||||
|
ChannelIdentity identity = createIdentity();
|
||||||
|
ChannelAccount account = createWecomAgentAccount(30L, 0);
|
||||||
|
|
||||||
|
when(personMapper.selectById(1L)).thenReturn(person);
|
||||||
when(channelIdentityMapper.selectOne(any())).thenReturn(identity);
|
when(channelIdentityMapper.selectOne(any())).thenReturn(identity);
|
||||||
when(accountMapper.selectList(any())).thenReturn(Collections.singletonList(account));
|
when(accountMapper.selectList(any())).thenReturn(Collections.singletonList(account));
|
||||||
when(accountMapper.selectById(30L)).thenReturn(account);
|
when(accountMapper.selectById(30L)).thenReturn(account);
|
||||||
@@ -159,10 +191,12 @@ class SendServiceResolveTargetTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolvePersonTarget_rejectsCrossTenantAccount() {
|
void resolvePersonTarget_rejectsCrossTenantAccount() {
|
||||||
|
Person person = createPerson();
|
||||||
ChannelIdentity identity = createIdentity();
|
ChannelIdentity identity = createIdentity();
|
||||||
ChannelAccount account = createAccount(30L, "wecom", 1);
|
ChannelAccount account = createWecomAgentAccount(30L, 1);
|
||||||
account.setTenantCode("other");
|
account.setTenantCode("other");
|
||||||
|
|
||||||
|
when(personMapper.selectById(1L)).thenReturn(person);
|
||||||
when(channelIdentityMapper.selectOne(any())).thenReturn(identity);
|
when(channelIdentityMapper.selectOne(any())).thenReturn(identity);
|
||||||
when(accountMapper.selectList(any())).thenReturn(Collections.singletonList(account));
|
when(accountMapper.selectList(any())).thenReturn(Collections.singletonList(account));
|
||||||
when(accountMapper.selectById(30L)).thenReturn(account);
|
when(accountMapper.selectById(30L)).thenReturn(account);
|
||||||
@@ -174,16 +208,36 @@ class SendServiceResolveTargetTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolvePersonTarget_rejectsChannelTypeMismatch() {
|
void resolvePersonTarget_rejectsChannelTypeMismatch() {
|
||||||
|
Person person = createPerson();
|
||||||
ChannelIdentity identity = createIdentity();
|
ChannelIdentity identity = createIdentity();
|
||||||
ChannelAccount account = createAccount(30L, "mobile", 1);
|
ChannelAccount account = createAccount(30L, "wecom", 1);
|
||||||
|
account.setAccountType(3);
|
||||||
|
|
||||||
|
when(personMapper.selectById(1L)).thenReturn(person);
|
||||||
|
when(channelIdentityMapper.selectOne(any())).thenReturn(identity);
|
||||||
|
when(accountMapper.selectList(any())).thenReturn(Collections.singletonList(account));
|
||||||
|
|
||||||
|
BusinessException ex = assertThrows(BusinessException.class,
|
||||||
|
() -> sendService.resolveTestTargets("tenant", createPersonRequest()));
|
||||||
|
assertEquals("租户未配置可用发送账号: channel=wecom", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvePersonTarget_selectsWecomAgentAccount() {
|
||||||
|
Person person = createPerson();
|
||||||
|
ChannelIdentity identity = createIdentity();
|
||||||
|
ChannelAccount account = createWecomAgentAccount(30L, 1);
|
||||||
|
|
||||||
|
when(personMapper.selectById(1L)).thenReturn(person);
|
||||||
when(channelIdentityMapper.selectOne(any())).thenReturn(identity);
|
when(channelIdentityMapper.selectOne(any())).thenReturn(identity);
|
||||||
when(accountMapper.selectList(any())).thenReturn(Collections.singletonList(account));
|
when(accountMapper.selectList(any())).thenReturn(Collections.singletonList(account));
|
||||||
when(accountMapper.selectById(30L)).thenReturn(account);
|
when(accountMapper.selectById(30L)).thenReturn(account);
|
||||||
|
|
||||||
BusinessException ex = assertThrows(BusinessException.class,
|
List<SendTarget> targets = sendService.resolveTestTargets("tenant", createPersonRequest());
|
||||||
() -> sendService.resolveTestTargets("tenant", createPersonRequest()));
|
|
||||||
assertEquals("租户没有可用的发送账号: channel=wecom", ex.getMessage());
|
assertEquals(1, targets.size());
|
||||||
|
assertEquals(Long.valueOf(30L), targets.get(0).getAccountId());
|
||||||
|
assertEquals(ChannelType.WECOM, targets.get(0).getChannelType());
|
||||||
}
|
}
|
||||||
|
|
||||||
private SendTestRequest createConversationRequest(String strategy) {
|
private SendTestRequest createConversationRequest(String strategy) {
|
||||||
@@ -217,16 +271,24 @@ class SendServiceResolveTargetTest {
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Conversation createConversation() {
|
private Conversation createConversation(Integer scene) {
|
||||||
Conversation conversation = new Conversation();
|
Conversation conversation = new Conversation();
|
||||||
conversation.setId(1L);
|
conversation.setId(1L);
|
||||||
conversation.setTenantCode("tenant");
|
conversation.setTenantCode("tenant");
|
||||||
conversation.setChannelType("wecom");
|
conversation.setChannelType("wecom");
|
||||||
|
conversation.setConversationScene(scene);
|
||||||
conversation.setChannelConversationId("conv-1");
|
conversation.setChannelConversationId("conv-1");
|
||||||
conversation.setStatus(1);
|
conversation.setStatus(1);
|
||||||
return conversation;
|
return conversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Person createPerson() {
|
||||||
|
Person person = new Person();
|
||||||
|
person.setId(1L);
|
||||||
|
person.setTenantCode("tenant");
|
||||||
|
return person;
|
||||||
|
}
|
||||||
|
|
||||||
private ChannelAccount createAccount(Long id, String channelType, int status) {
|
private ChannelAccount createAccount(Long id, String channelType, int status) {
|
||||||
ChannelAccount account = new ChannelAccount();
|
ChannelAccount account = new ChannelAccount();
|
||||||
account.setId(id);
|
account.setId(id);
|
||||||
@@ -236,6 +298,27 @@ class SendServiceResolveTargetTest {
|
|||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ChannelAccount createWecomBotAccount(Long id, int status) {
|
||||||
|
ChannelAccount account = createAccount(id, "wecom", status);
|
||||||
|
account.setAccountType(6);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChannelAccount createWecomAgentAccount(Long id, int status) {
|
||||||
|
ChannelAccount account = createAccount(id, "wecom", status);
|
||||||
|
account.setAccountType(2);
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChannelAccount createPywechatAccount(Long id, int status) {
|
||||||
|
ChannelAccount account = createAccount(id, "wechat_personal", status);
|
||||||
|
account.setAccountType(3);
|
||||||
|
account.setOnlineStatus(1);
|
||||||
|
account.setRiskLevel(1);
|
||||||
|
account.setLastHeartbeat(java.time.LocalDateTime.now());
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
private ChannelIdentity createIdentity() {
|
private ChannelIdentity createIdentity() {
|
||||||
ChannelIdentity identity = new ChannelIdentity();
|
ChannelIdentity identity = new ChannelIdentity();
|
||||||
identity.setTenantCode("tenant");
|
identity.setTenantCode("tenant");
|
||||||
|
|||||||
Reference in New Issue
Block a user