基于DDD+Spring Boot 3.2+LangChain4j构建企业级智能客服系统
本文介绍了一个基于领域驱动设计(DDD)的电商智能客服系统开发方案。系统采用Spring Boot 3.2.0框架,集成通义千问大模型和LangChain4j开发框架,使用Java 21特性构建。文章详细阐述了DDD核心概念在智能客服场景中的映射关系,包括限界上下文、聚合根、值对象等。提供了规范的工程目录结构,明确划分应用层、领域层、基础设施层等职责。同时展示了适配JDK21和Spring Boo
·
一、概述
在大模型应用开发中,传统的三层架构(Controller-Service-Dao)难以应对复杂业务场景的领域边界划分、业务规则沉淀等问题。本文以「电商智能客服系统」为例,结合领域驱动设计(DDD) 思想,基于Spring Boot 3.2.0、JDK21、LangChain4j和通义千问,构建高内聚、低耦合的企业级智能客服系统,同时兼顾大模型集成、领域规则沉淀、可扩展性等核心诉求。

核心技术栈
- 框架:Spring Boot 3.2.0(适配JDK21)
- 大模型:通义千问(qwen-turbo)
- 大模型开发框架:LangChain4j 0.32.0
- 设计模式:领域驱动设计(DDD)
- 语言:Java 21(Record、文本块、虚拟线程等特性)
二、DDD核心概念适配(智能客服场景)
在智能客服场景中,我们先明确DDD核心概念的映射关系,确保设计落地有依据:
| DDD核心概念 | 智能客服场景映射 |
|---|---|
| 限界上下文 | 客服交互上下文(核心域):包含客服对话、AI回答、订单查询等核心能力 |
| 聚合根 | 聊天会话(ChatSession):包含会话ID、用户ID、对话记录等,是客服交互的核心聚合根 |
| 值对象 | 用户提问(UserQuery)、客服回答(CustomerAnswer)、订单信息(OrderInfo):无唯一标识,属性不可变 |
| 领域服务 | 客服领域服务(CustomerDomainService):封装核心业务规则(如对话记忆、AI回答生成) |
| 应用服务 | 客服交互应用服务(CustomerInteractionAppService):协调领域层与基础设施层,处理用户请求 |
| 仓储 | 聊天会话仓储(ChatSessionRepository):定义聚合根的持久化接口,基础设施层实现 |
| 基础设施层 | 通义千问客户端、内存仓储实现、配置管理等 |
| 接口层 | REST API接口,接收前端/客户端请求 |
三、工程目录结构(DDD规范)
严格遵循DDD「分层+分包」原则,工程目录结构如下(按职责划分,符合企业级规范):
com.ecommerce.customer.service (根包,对应客服交互限界上下文)
├── CustomerServiceApplication.java // 应用启动类
├── application/ // 应用层:协调领域层,处理用例
│ ├── command/ // 命令对象(写操作)
│ │ └── CustomerChatCommand.java // 客服对话命令
│ ├── dto/ // 数据传输对象
│ │ ├── ChatRequestDTO.java // 对话请求DTO
│ │ └── ChatResponseDTO.java // 对话响应DTO
│ └── service/ // 应用服务
│ └── CustomerInteractionAppService.java
├── domain/ // 领域层:核心业务逻辑,领域模型
│ ├── aggregate/ // 聚合根
│ │ └── ChatSession.java // 聊天会话聚合根
│ ├── event/ // 领域事件(可选,这里简化)
│ ├── repository/ // 仓储接口(领域层定义,基础设施层实现)
│ │ └── ChatSessionRepository.java
│ ├── service/ // 领域服务:封装跨聚合/值对象的业务规则
│ │ └── CustomerDomainService.java
│ └── valueobject/ // 值对象:无唯一标识,不可变
│ ├── CustomerAnswer.java
│ ├── OrderInfo.java
│ └── UserQuery.java
├── infrastructure/ // 基础设施层:外部依赖实现、仓储实现
│ ├── config/ // 配置类
│ │ └── DddSmartServiceConfig.java
│ ├── client/ // 外部服务客户端(通义千问)
│ │ └── QwenAiClient.java
│ ├── persistence/ // 仓储实现
│ │ └── InMemoryChatSessionRepository.java
│ └── tool/ // 外部工具集成(订单查询)
│ └── OrderQueryTool.java
├── interfaces/ // 接口层:对外暴露的接口(REST/Feign等)
│ └── rest/ // REST接口
│ ├── CustomerServiceController.java
│ └── request/ // 接口请求模型
│ └── ChatRequest.java
└── common/ // 公共层:通用工具、异常、常量
├── constant/ // 常量
│ └── CustomerServiceConstant.java
├── exception/ // 自定义异常
│ ├── ApplicationException.java
│ └── DomainException.java
└── util/ // 通用工具
└── StringUtils.java
四、核心配置文件
4.1 pom.xml(Maven依赖,适配JDK21+Spring Boot 3.2.0)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Spring Boot 3.2.0 父工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.ecommerce</groupId>
<artifactId>customer-service-ddd</artifactId>
<version>1.0.0</version>
<name>CustomerServiceDDD</name>
<description>DDD模式智能客服系统(Spring Boot 3.2 + LangChain4j + 通义千问)</description>
<!-- 核心配置 -->
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<langchain4j.version>0.32.0</langchain4j.version>
<commons-lang3.version>3.14.0</commons-lang3.version>
</properties>
<dependencies>
<!-- Spring Boot 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- LangChain4j 核心 + 通义千问适配器 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-dashscope</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Spring Boot 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<!-- JDK21 编译插件 -->
<plugin>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</plugin>
</plugins>
</build>
</project>
4.2 application.yml(配置文件,敏感信息隔离)
# 服务器配置
server:
port: 8080
servlet:
context-path: /customer-service
tomcat:
uri-encoding: UTF-8
# 通义千问配置(基础设施层用)
dashscope:
api-key: ${DASHSCOPE_API_KEY:sk-266219f347304b41b9249ce6becd35850} # 环境变量优先
model: qwen-turbo
temperature: 0.6
max-tokens: 1000
top-p: 0.8
# 对话记忆配置(领域层用)
memory:
window:
size: 10
# 日志配置(DDD规范:分层日志隔离)
logging:
charset:
console: UTF-8
file: UTF-8
level:
root: INFO
com.ecommerce.customer.service.domain: DEBUG # 领域层日志
com.ecommerce.customer.service.application: INFO # 应用层日志
com.ecommerce.customer.service.infrastructure: WARN # 基础设施层日志
com.ecommerce.customer.service.interfaces: INFO # 接口层日志
com.alibaba.dashscope: WARN
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{60} - %msg%n"
五、分层核心代码实现
5.1 公共层(common)
5.1.1 自定义异常(DomainException.java)
package com.ecommerce.customer.service.common.exception;
/**
* 领域异常:领域层业务规则违反时抛出
*/
public class DomainException extends RuntimeException {
public DomainException(String message) {
super(message);
}
public DomainException(String message, Throwable cause) {
super(message, cause);
}
}
5.1.2 应用异常(ApplicationException.java)
package com.ecommerce.customer.service.common.exception;
/**
* 应用异常:应用层处理失败时抛出
*/
public class ApplicationException extends RuntimeException {
public ApplicationException(String message) {
super(message);
}
public ApplicationException(String message, Throwable cause) {
super(message, cause);
}
}
5.1.3 通用工具(StringUtils.java)
package com.ecommerce.customer.service.common.util;
import org.apache.commons.lang3.StringUtils;
/**
* 字符串工具类(封装第三方工具,领域层解耦)
*/
public class StringUtils extends StringUtils {
/**
* 脱敏处理(如API Key、手机号)
*/
public static String desensitize(String content, int keepLength) {
if (isEmpty(content)) {
return "";
}
if (content.length() <= keepLength) {
return content;
}
return content.substring(0, keepLength) + "****";
}
}
5.2 领域层(domain)
5.2.1 值对象(UserQuery.java,JDK21 Record)
package com.ecommerce.customer.service.domain.valueobject;
import java.time.LocalDateTime;
/**
* 用户提问值对象:无唯一标识,不可变
*/
public record UserQuery(
String content, // 提问内容
LocalDateTime createTime // 提问时间
) {
// 工厂方法:保证值对象合法性(DDD值对象规则)
public static UserQuery create(String content) {
if (com.ecommerce.customer.service.common.util.StringUtils.isBlank(content)) {
throw new com.ecommerce.customer.service.common.exception.DomainException("用户提问内容不能为空");
}
return new UserQuery(content, LocalDateTime.now());
}
}
5.2.2 值对象(CustomerAnswer.java)
package com.ecommerce.customer.service.domain.valueobject;
import java.time.LocalDateTime;
/**
* 客服回答值对象
*/
public record CustomerAnswer(
String content, // 回答内容
LocalDateTime createTime // 回答时间
) {
// 工厂方法
public static CustomerAnswer create(String content) {
return new CustomerAnswer(content, LocalDateTime.now());
}
}
5.2.3 值对象(OrderInfo.java)
package com.ecommerce.customer.service.domain.valueobject;
import java.time.LocalDateTime;
/**
* 订单信息值对象(支撑域:订单查询)
*/
public record OrderInfo(
String orderId, // 订单号
String productName, // 商品名称
String size, // 尺码
String status, // 订单状态
LocalDateTime createTime // 创建时间
) {
// 工厂方法:校验订单号合法性
public static OrderInfo create(String orderId, String productName, String size, String status, LocalDateTime createTime) {
if (com.ecommerce.customer.service.common.util.StringUtils.isBlank(orderId)) {
throw new com.ecommerce.customer.service.common.exception.DomainException("订单号不能为空");
}
return new OrderInfo(orderId, productName, size, status, createTime);
}
}
5.2.4 聚合根(ChatSession.java)
package com.ecommerce.customer.service.domain.aggregate;
import com.ecommerce.customer.service.domain.valueobject.CustomerAnswer;
import com.ecommerce.customer.service.domain.valueobject.UserQuery;
import com.ecommerce.customer.service.common.exception.DomainException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 聊天会话聚合根(DDD核心:聚合根,包含业务规则)
* 聚合边界:ChatSession + UserQuery + CustomerAnswer
*/
public class ChatSession {
// 聚合根ID(唯一标识)
private final String sessionId;
// 用户ID(外部聚合根ID,关联用户域)
private final String userId;
// 对话记录(聚合内的实体/值对象)
private final List<DialogueRecord> dialogueRecords;
// 创建时间
private final LocalDateTime createTime;
// 最后更新时间
private LocalDateTime lastUpdateTime;
// 内部值对象:对话记录(聚合内私有)
private record DialogueRecord(
UserQuery userQuery,
CustomerAnswer customerAnswer
) {}
// 聚合根工厂方法(DDD规范:聚合根只能通过工厂方法创建)
public static ChatSession create(String userId) {
if (com.ecommerce.customer.service.common.util.StringUtils.isBlank(userId)) {
throw new DomainException("用户ID不能为空");
}
return new ChatSession(
UUID.randomUUID().toString(),
userId,
new ArrayList<>(),
LocalDateTime.now(),
LocalDateTime.now()
);
}
// 私有构造器(禁止外部new)
private ChatSession(String sessionId, String userId, List<DialogueRecord> dialogueRecords, LocalDateTime createTime, LocalDateTime lastUpdateTime) {
this.sessionId = sessionId;
this.userId = userId;
this.dialogueRecords = dialogueRecords;
this.createTime = createTime;
this.lastUpdateTime = lastUpdateTime;
}
// 领域行为:添加对话记录(封装业务规则)
public void addDialogue(UserQuery userQuery, CustomerAnswer customerAnswer) {
// 业务规则:对话记录数量不超过记忆窗口大小
int memoryWindowSize = Integer.parseInt(System.getenv("DASHSCOPE_MEMORY_WINDOW_SIZE") != null
? System.getenv("DASHSCOPE_MEMORY_WINDOW_SIZE")
: "10");
if (dialogueRecords.size() >= memoryWindowSize) {
// 移除最早的记录(FIFO)
dialogueRecords.remove(0);
}
dialogueRecords.add(new DialogueRecord(userQuery, customerAnswer));
this.lastUpdateTime = LocalDateTime.now();
}
// 领域行为:获取历史对话(供领域服务使用)
public List<String> getHistoryDialogue() {
List<String> history = new ArrayList<>();
for (DialogueRecord record : dialogueRecords) {
history.add("用户:" + record.userQuery().content());
history.add("客服:" + record.customerAnswer().content());
}
return history;
}
// 聚合根属性访问器(DDD规范:仅暴露必要的getter)
public String getSessionId() {
return sessionId;
}
public String getUserId() {
return userId;
}
public LocalDateTime getLastUpdateTime() {
return lastUpdateTime;
}
}
5.2.5 仓储接口(ChatSessionRepository.java)
package com.ecommerce.customer.service.domain.repository;
import com.ecommerce.customer.service.domain.aggregate.ChatSession;
import java.util.Optional;
/**
* 聊天会话仓储接口(领域层定义,基础设施层实现)
* DDD规范:仓储接口属于领域层,实现属于基础设施层
*/
public interface ChatSessionRepository {
/**
* 保存/更新聚合根
*/
ChatSession save(ChatSession chatSession);
/**
* 根据用户ID查询会话
*/
Optional<ChatSession> findByUserId(String userId);
}
5.2.6 领域服务(CustomerDomainService.java)
package com.ecommerce.customer.service.domain.service;
import com.ecommerce.customer.service.domain.aggregate.ChatSession;
import com.ecommerce.customer.service.domain.repository.ChatSessionRepository;
import com.ecommerce.customer.service.domain.valueobject.CustomerAnswer;
import com.ecommerce.customer.service.domain.valueobject.UserQuery;
import com.ecommerce.customer.service.common.exception.DomainException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 客服领域服务(DDD规范:封装跨聚合/值对象的业务规则)
* 核心职责:生成AI回答、维护对话会话
*/
@Service
public class CustomerDomainService {
private static final Logger log = LoggerFactory.getLogger(CustomerDomainService.class);
// 依赖仓储接口(领域层仅依赖接口,解耦基础设施实现)
private final ChatSessionRepository chatSessionRepository;
// 依赖AI客户端(基础设施层实现,通过构造器注入)
private final com.ecommerce.customer.service.infrastructure.client.QwenAiClient qwenAiClient;
// 依赖订单查询工具(基础设施层)
private final com.ecommerce.customer.service.infrastructure.tool.OrderQueryTool orderQueryTool;
// 构造器注入(DDD+Spring规范)
public CustomerDomainService(ChatSessionRepository chatSessionRepository,
com.ecommerce.customer.service.infrastructure.client.QwenAiClient qwenAiClient,
com.ecommerce.customer.service.infrastructure.tool.OrderQueryTool orderQueryTool) {
this.chatSessionRepository = chatSessionRepository;
this.qwenAiClient = qwenAiClient;
this.orderQueryTool = orderQueryTool;
}
/**
* 核心领域行为:处理用户提问,生成回答
*/
public CustomerAnswer handleUserQuery(String userId, UserQuery userQuery) {
// 1. 获取/创建用户会话(聚合根操作)
ChatSession chatSession = chatSessionRepository.findByUserId(userId)
.orElseGet(() -> ChatSession.create(userId));
log.debug("领域服务:处理用户[{}]会话,会话ID:{}", userId, chatSession.getSessionId());
// 2. 构建上下文(历史对话+当前提问)
List<String> historyDialogue = chatSession.getHistoryDialogue();
String prompt = buildPrompt(historyDialogue, userQuery.content());
// 3. 调用AI生成回答(基础设施层能力)
String aiAnswer = qwenAiClient.generateAnswer(prompt);
CustomerAnswer customerAnswer = CustomerAnswer.create(aiAnswer);
// 4. 更新会话(聚合根领域行为)
chatSession.addDialogue(userQuery, customerAnswer);
chatSessionRepository.save(chatSession);
return customerAnswer;
}
/**
* 领域规则:构建AI Prompt(封装Prompt工程)
*/
private String buildPrompt(List<String> historyDialogue, String currentQuery) {
// 系统提示词(领域规则)
String systemPrompt = """
你是某电商平台的智能客服,需遵守以下规则:
1. 语气友好、专业,使用中文回复;
2. 优先回答商品退换货、物流时效等通用问题:
- 退换货政策:签收后7天内无理由退货,质量问题运费商家承担,非质量问题用户承担;
- 物流时效:江浙沪次日达,其他地区3-5天,偏远地区7天;
3. 若用户询问订单,调用订单查询工具获取详情;
4. 记住用户上下文,支持多轮对话;
5. 无法回答的问题引导联系人工客服(400-123-4567)。
""";
// 拼接历史对话+当前提问
StringBuilder promptBuilder = new StringBuilder(systemPrompt);
promptBuilder.append("\n【历史对话】:\n");
historyDialogue.forEach(line -> promptBuilder.append(line).append("\n"));
promptBuilder.append("\n【当前提问】:").append(currentQuery);
return promptBuilder.toString();
}
}
5.3 基础设施层(infrastructure)
5.3.1 通义千问客户端(QwenAiClient.java)
package com.ecommerce.customer.service.infrastructure.client;
import com.ecommerce.customer.service.common.exception.ApplicationException;
import com.ecommerce.customer.service.common.util.StringUtils;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.dashscope.QwenChatModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* 通义千问AI客户端(基础设施层:外部服务集成)
* DDD规范:基础设施层封装外部依赖,领域层不感知具体实现
*/
@Component
public class QwenAiClient {
private static final Logger log = LoggerFactory.getLogger(QwenAiClient.class);
// 从配置文件注入参数
@Value("${dashscope.api-key}")
private String apiKey;
@Value("${dashscope.model:qwen-turbo}")
private String modelName;
@Value("${dashscope.temperature:0.6}")
private Float temperature;
@Value("${dashscope.max-tokens:1000}")
private Integer maxTokens;
@Value("${dashscope.top-p:0.8}")
private Float topP;
// 构建AI模型(懒加载)
private ChatLanguageModel qwenChatModel;
/**
* 生成AI回答(对外暴露的能力)
*/
public String generateAnswer(String prompt) {
try {
// 初始化模型(懒加载)
if (qwenChatModel == null) {
initQwenModel();
}
// 调用LangChain4j生成回答
return qwenChatModel.generate(dev.langchain4j.message.ChatMessage.user(prompt)).content();
} catch (Exception e) {
log.error("通义千问调用失败,prompt:{}", prompt.substring(0, Math.min(prompt.length(), 100)), e);
throw new ApplicationException("AI回答生成失败:" + e.getMessage());
}
}
/**
* 初始化通义千问模型(基础设施层内部实现)
*/
private void initQwenModel() {
// 校验API Key
if (StringUtils.isBlank(apiKey)) {
throw new ApplicationException("通义千问API Key未配置");
}
log.info("初始化通义千问模型:{},API Key:{}", modelName, StringUtils.desensitize(apiKey, 6));
// 构建模型(LangChain4j)
qwenChatModel = QwenChatModel.builder()
.apiKey(apiKey)
.modelName(modelName)
.temperature(temperature)
.maxTokens(maxTokens)
.topP(topP)
.build();
}
}
5.3.2 订单查询工具(OrderQueryTool.java)
package com.ecommerce.customer.service.infrastructure.tool;
import com.ecommerce.customer.service.domain.valueobject.OrderInfo;
import com.ecommerce.customer.service.common.exception.DomainException;
import dev.langchain4j.agent.tool.Tool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 订单查询工具(基础设施层:外部工具集成)
* 模拟对接真实订单系统,生产环境可替换为MyBatis/Feign
*/
@Component
public class OrderQueryTool {
private static final Logger log = LoggerFactory.getLogger(OrderQueryTool.class);
// 模拟订单数据库
private static final Map<String, OrderInfo> ORDER_DB = new ConcurrentHashMap<>();
// 初始化测试数据
static {
ORDER_DB.put("ORD123456", OrderInfo.create(
"ORD123456",
"男士纯棉T恤",
"XL",
"已发货",
LocalDateTime.now().minusDays(2)
));
ORDER_DB.put("ORD789012", OrderInfo.create(
"ORD789012",
"女士连衣裙",
"M",
"待发货",
LocalDateTime.now()
));
}
/**
* LangChain4j工具方法(供AI调用)
*/
@Tool(value = "查询订单信息", description = "根据订单号查询订单详情,参数为订单号(如ORD123456)")
public OrderInfo queryOrder(String orderId) {
log.info("订单查询工具:查询订单号{}", orderId);
if (StringUtils.isBlank(orderId)) {
throw new DomainException("订单号不能为空");
}
OrderInfo orderInfo = ORDER_DB.get(orderId);
if (orderInfo == null) {
throw new DomainException("订单不存在:" + orderId);
}
return orderInfo;
}
}
5.3.3 仓储实现(InMemoryChatSessionRepository.java)
package com.ecommerce.customer.service.infrastructure.persistence;
import com.ecommerce.customer.service.domain.aggregate.ChatSession;
import com.ecommerce.customer.service.domain.repository.ChatSessionRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* 内存版聊天会话仓储实现(基础设施层)
* 生产环境可替换为Redis/MongoDB/MySQL实现
*/
@Repository
public class InMemoryChatSessionRepository implements ChatSessionRepository {
private static final Logger log = LoggerFactory.getLogger(InMemoryChatSessionRepository.class);
// 内存存储(模拟持久化)
private final Map<String, ChatSession> sessionMap = new ConcurrentHashMap<>();
@Override
public ChatSession save(ChatSession chatSession) {
log.debug("仓储实现:保存会话,用户ID:{},会话ID:{}", chatSession.getUserId(), chatSession.getSessionId());
sessionMap.put(chatSession.getUserId(), chatSession);
return chatSession;
}
@Override
public Optional<ChatSession> findByUserId(String userId) {
log.debug("仓储实现:查询用户{}的会话", userId);
return Optional.ofNullable(sessionMap.get(userId));
}
}
5.3.4 配置类(DddSmartServiceConfig.java)
package com.ecommerce.customer.service.infrastructure.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* DDD配置类(基础设施层)
* 启用配置属性绑定,整合外部依赖
*/
@Configuration
@EnableConfigurationProperties
public class DddSmartServiceConfig {
// 基础设施层配置:如AI模型、仓储、工具等由Spring自动装配
}
5.4 应用层(application)
5.4.1 命令对象(CustomerChatCommand.java)
package com.ecommerce.customer.service.application.command;
/**
* 客服对话命令(应用层:写操作命令对象)
* DDD规范:命令对象封装写操作参数
*/
public record CustomerChatCommand(
String userId, // 用户ID
String userQuery // 用户提问内容
) {
// 命令校验
public void validate() {
if (StringUtils.isBlank(userId)) {
throw new com.ecommerce.customer.service.common.exception.ApplicationException("用户ID不能为空");
}
if (StringUtils.isBlank(userQuery)) {
throw new com.ecommerce.customer.service.common.exception.ApplicationException("用户提问不能为空");
}
}
}
5.4.2 DTO(ChatRequestDTO.java)
package com.ecommerce.customer.service.application.dto;
/**
* 对话请求DTO(应用层:数据传输对象)
*/
public record ChatRequestDTO(
String userId,
String userQuery
) {}
5.4.3 DTO(ChatResponseDTO.java)
package com.ecommerce.customer.service.application.dto;
/**
* 对话响应DTO
*/
public record ChatResponseDTO(
String answer,
String sessionId,
long responseTime // 响应耗时(毫秒)
) {}
5.4.4 应用服务(CustomerInteractionAppService.java)
package com.ecommerce.customer.service.application.service;
import com.ecommerce.customer.service.application.command.CustomerChatCommand;
import com.ecommerce.customer.service.application.dto.ChatRequestDTO;
import com.ecommerce.customer.service.application.dto.ChatResponseDTO;
import com.ecommerce.customer.service.domain.service.CustomerDomainService;
import com.ecommerce.customer.service.domain.valueobject.UserQuery;
import com.ecommerce.customer.service.common.exception.ApplicationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
/**
* 客服交互应用服务(应用层:协调领域层,处理用户用例)
* DDD规范:应用服务不包含业务规则,仅做协调、参数转换、事务管理
*/
@Service
public class CustomerInteractionAppService {
private static final Logger log = LoggerFactory.getLogger(CustomerInteractionAppService.class);
// 依赖领域服务(应用层依赖领域层)
private final CustomerDomainService customerDomainService;
// 构造器注入
public CustomerInteractionAppService(CustomerDomainService customerDomainService) {
this.customerDomainService = customerDomainService;
}
/**
* 处理客服对话请求(核心应用服务)
*/
public ChatResponseDTO handleChat(ChatRequestDTO requestDTO) {
long startTime = System.currentTimeMillis();
try {
// 1. 转换为命令对象
CustomerChatCommand command = new CustomerChatCommand(
requestDTO.userId(),
requestDTO.userQuery()
);
// 2. 命令校验
command.validate();
log.info("应用服务:处理用户{}的对话请求,提问:{}", command.userId(), command.userQuery());
// 3. 转换为领域对象
UserQuery userQuery = UserQuery.create(command.userQuery());
// 4. 调用领域服务(核心业务逻辑)
var customerAnswer = customerDomainService.handleUserQuery(command.userId(), userQuery);
// 5. 构建响应DTO(应用层转换)
long responseTime = System.currentTimeMillis() - startTime;
return new ChatResponseDTO(
customerAnswer.content(),
// 此处简化,实际应从会话聚合根获取
"SESSION-" + command.userId(),
responseTime
);
} catch (Exception e) {
log.error("应用服务:处理对话请求失败", e);
throw new ApplicationException("对话处理失败:" + e.getMessage());
}
}
}
5.5 接口层(interfaces)
5.5.1 请求模型(ChatRequest.java)
package com.ecommerce.customer.service.interfaces.rest.request;
/**
* REST接口请求模型(接口层:对外暴露的请求模型)
*/
public record ChatRequest(
String userId,
String userQuery
) {}
5.5.2 REST控制器(CustomerServiceController.java)
package com.ecommerce.customer.service.interfaces.rest;
import com.ecommerce.customer.service.application.dto.ChatRequestDTO;
import com.ecommerce.customer.service.application.dto.ChatResponseDTO;
import com.ecommerce.customer.service.application.service.CustomerInteractionAppService;
import com.ecommerce.customer.service.interfaces.rest.request.ChatRequest;
import com.ecommerce.customer.service.common.exception.ApplicationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 客服服务REST控制器(接口层:对外暴露API)
* DDD规范:接口层仅做参数接收、转换,调用应用服务
*/
@RestController
@RequestMapping("/api/v1/chat")
public class CustomerServiceController {
private static final Logger log = LoggerFactory.getLogger(CustomerServiceController.class);
// 依赖应用服务(接口层依赖应用层)
private final CustomerInteractionAppService customerInteractionAppService;
// 构造器注入
public CustomerServiceController(CustomerInteractionAppService customerInteractionAppService) {
this.customerInteractionAppService = customerInteractionAppService;
}
/**
* 客服对话接口
*/
@PostMapping
public ResponseEntity<?> chat(@RequestBody ChatRequest request) {
try {
// 1. 接口请求模型转换为应用层DTO
ChatRequestDTO requestDTO = new ChatRequestDTO(
request.userId(),
request.userQuery()
);
// 2. 调用应用服务
ChatResponseDTO responseDTO = customerInteractionAppService.handleChat(requestDTO);
// 3. 构建响应
return ResponseEntity.ok(Map.of(
"code", 200,
"message", "success",
"data", responseDTO
));
} catch (ApplicationException e) {
log.error("接口层:处理对话请求失败", e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of(
"code", 400,
"message", e.getMessage(),
"data", null
));
} catch (Exception e) {
log.error("接口层:系统异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of(
"code", 500,
"message", "系统内部错误",
"data", null
));
}
}
}
5.6 启动类(CustomerServiceApplication.java)
package com.ecommerce.customer.service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 应用启动类(DDD限界上下文入口)
*/
@SpringBootApplication
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication.class, args);
System.out.println("===== DDD模式智能客服系统启动成功(端口:8080) =====");
}
}
六、部署与测试
6.1 启动应用
# 1. 配置环境变量(生产环境)
export DASHSCOPE_API_KEY=你的通义千问API Key
# 2. 打包应用
mvn clean package -DskipTests
# 3. 启动应用
java -jar target/customer-service-ddd-1.0.0.jar
6.2 接口测试(Curl)
curl -X POST http://localhost:8080/customer-service/api/v1/chat \
-H "Content-Type: application/json" \
-d '{
"userId": "USER001",
"userQuery": "查询订单ORD123456"
}'
6.3 预期响应
{
"code": 200,
"message": "success",
"data": {
"answer": "你查询的订单信息如下:\n订单号:ORD123456\n商品名称:男士纯棉T恤\n尺码:XL\n状态:已发货\n创建时间:2025-12-22T10:05:30",
"sessionId": "SESSION-USER001",
"responseTime": 500
}
}
七、DDD落地要点总结
- 分层隔离:严格遵循「接口层→应用层→领域层→基础设施层」依赖方向,领域层不依赖任何外层;
- 聚合根设计:ChatSession作为聚合根,封装内部业务规则,仅通过工厂方法创建;
- 值对象不可变:使用JDK21 Record实现值对象,通过工厂方法保证合法性;
- 仓储解耦:领域层定义仓储接口,基础设施层实现,便于替换持久化方式;
- 业务规则沉淀:所有核心业务规则(如对话记忆、Prompt构建)沉淀在领域层,应用层仅做协调;
- 外部依赖封装:通义千问、订单查询等外部依赖封装在基础设施层,领域层无感知。
八、总结
本文基于DDD思想重构了智能客服系统,结合Spring Boot 3.2.0、JDK21、LangChain4j和通义千问,实现了「领域驱动+大模型」的企业级应用开发。核心价值在于:
- 业务可维护性:领域层沉淀核心业务规则,脱离技术框架依赖;
- 可扩展性:限界上下文、分层设计便于后续扩展多模型、多渠道客服;
- 可测试性:领域层可独立测试,无需依赖外部服务;
- 技术解耦:基础设施层封装外部依赖,便于替换AI模型、持久化方式。
该架构可直接复用至金融、政务等领域的智能问答系统,仅需调整领域层业务规则和Prompt即可快速适配。
更多推荐


所有评论(0)