package vip.mate.channel.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import vip.mate.channel.model.ChannelEntity; import vip.mate.channel.repository.ChannelMapper; import vip.mate.exception.MateClawException; import java.security.SecureRandom; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 渠道业务服务 *

* 负责渠道的 CRUD 管理。 * 渠道的运行时生命周期由 ChannelManager 管理。 * * @author MateClaw Team */ @Slf4j @Service @RequiredArgsConstructor public class ChannelService { private final ChannelMapper channelMapper; private final ObjectMapper objectMapper; private static final SecureRandom SECURE_RANDOM = new SecureRandom(); /** * 获取所有渠道列表 */ public List listChannels() { return channelMapper.selectList(new LambdaQueryWrapper() .orderByDesc(ChannelEntity::getEnabled) .orderByDesc(ChannelEntity::getCreateTime)); } /** * 按工作区列出渠道 */ public List listChannelsByWorkspace(Long workspaceId) { return channelMapper.selectList(new LambdaQueryWrapper() .eq(ChannelEntity::getWorkspaceId, workspaceId) .orderByDesc(ChannelEntity::getEnabled) .orderByDesc(ChannelEntity::getCreateTime)); } /** * 获取已启用的渠道列表(ChannelManager 启动时使用) */ public List listEnabledChannels() { return channelMapper.selectList(new LambdaQueryWrapper() .eq(ChannelEntity::getEnabled, true) .orderByAsc(ChannelEntity::getChannelType)); } /** * 按类型获取渠道列表(全局,向后兼容) */ public List listChannelsByType(String channelType) { return channelMapper.selectList(new LambdaQueryWrapper() .eq(ChannelEntity::getChannelType, channelType) .orderByDesc(ChannelEntity::getCreateTime)); } /** * 按类型和 workspace 获取渠道列表 */ public List listChannelsByTypeAndWorkspace(String channelType, Long workspaceId) { return channelMapper.selectList(new LambdaQueryWrapper() .eq(ChannelEntity::getChannelType, channelType) .eq(ChannelEntity::getWorkspaceId, workspaceId) .orderByDesc(ChannelEntity::getCreateTime)); } /** * 获取渠道详情 */ public ChannelEntity getChannel(Long id) { ChannelEntity channel = channelMapper.selectById(id); if (channel == null) { throw new MateClawException("err.channel.not_found", "渠道不存在: " + id); } return channel; } /** * 创建渠道 */ public ChannelEntity createChannel(ChannelEntity channel) { // 验证名称 if (channel.getName() == null || channel.getName().isBlank()) { throw new MateClawException("err.channel.name_required", "渠道名称不能为空"); } if (channel.getChannelType() == null || channel.getChannelType().isBlank()) { throw new MateClawException("err.channel.type_required", "渠道类型不能为空"); } if (channel.getEnabled() == null) { channel.setEnabled(false); } if ("webchat".equals(channel.getChannelType())) { channel.setConfigJson(enrichWebChatConfig(channel.getConfigJson(), null)); } channelMapper.insert(channel); log.info("Created channel: {} (type={})", channel.getName(), channel.getChannelType()); return channel; } /** * 更新渠道 */ public ChannelEntity updateChannel(ChannelEntity channel) { ChannelEntity existing = getChannel(channel.getId()); if ("webchat".equals(channel.getChannelType())) { channel.setConfigJson(enrichWebChatConfig(channel.getConfigJson(), existing.getConfigJson())); } channelMapper.updateById(channel); log.info("Updated channel: {}", existing.getName()); return channel; } /** * 删除渠道 */ public void deleteChannel(Long id) { ChannelEntity channel = getChannel(id); channelMapper.deleteById(id); log.info("Deleted channel: {}", channel.getName()); } /** * 启用/禁用渠道 */ public ChannelEntity toggleChannel(Long id, boolean enabled) { ChannelEntity channel = getChannel(id); channel.setEnabled(enabled); channelMapper.updateById(channel); log.info("Channel {} {}", channel.getName(), enabled ? "enabled" : "disabled"); return channel; } private String enrichWebChatConfig(String incomingConfigJson, String existingConfigJson) { Map incoming = parseConfig(incomingConfigJson); Map existing = parseConfig(existingConfigJson); String existingApiKey = asNonBlankString(existing.get("api_key")); incoming.put("api_key", existingApiKey != null ? existingApiKey : generateWebChatApiKey()); if (!incoming.containsKey("title") && existing.containsKey("title")) { incoming.put("title", existing.get("title")); } if (!incoming.containsKey("placeholder") && existing.containsKey("placeholder")) { incoming.put("placeholder", existing.get("placeholder")); } if (!incoming.containsKey("primary_color") && existing.containsKey("primary_color")) { incoming.put("primary_color", existing.get("primary_color")); } if (!incoming.containsKey("welcome_message") && existing.containsKey("welcome_message")) { incoming.put("welcome_message", existing.get("welcome_message")); } if (!incoming.containsKey("allowed_origins") && existing.containsKey("allowed_origins")) { incoming.put("allowed_origins", existing.get("allowed_origins")); } try { return objectMapper.writeValueAsString(incoming); } catch (Exception e) { throw new MateClawException("WebChat 渠道配置序列化失败: " + e.getMessage()); } } private Map parseConfig(String configJson) { if (configJson == null || configJson.isBlank()) { return new LinkedHashMap<>(); } try { return objectMapper.readValue(configJson, new TypeReference<>() {}); } catch (Exception e) { log.warn("Failed to parse channel configJson: {}", e.getMessage()); return new LinkedHashMap<>(); } } private String asNonBlankString(Object value) { if (value == null) return null; String text = String.valueOf(value).trim(); return text.isEmpty() ? null : text; } private String generateWebChatApiKey() { byte[] random = new byte[18]; SECURE_RANDOM.nextBytes(random); StringBuilder sb = new StringBuilder("mc_webchat_"); for (byte b : random) { sb.append(Character.forDigit((b >> 4) & 0xF, 16)); sb.append(Character.forDigit(b & 0xF, 16)); } return sb.toString(); } }