🛠️ 售后服务系统
# 🛠️ 售后服务系统
在跨境电商的完整交易链中,售后服务是维护客户关系、提升品牌价值的关键环节。当商品送达客户手中后,可能会遇到质量问题、尺寸不合适、或者客户不满意等情况。一个完善的售后服务系统能够快速响应客户需求,提供退换货、维修、投诉处理等服务,最终实现客户满意和品牌忠诚度的提升。
# 📋 系统概述
# 核心功能
- 退换货管理:支持多种退换货场景和流程
- 质量问题处理:快速响应和解决产品质量问题
- 客户投诉管理:系统化处理客户投诉和建议
- 保修服务管理:产品保修期内的维修服务
- 补偿方案管理:灵活的补偿策略和执行
- 客服工单系统:高效的客服工作流管理
- 满意度调研:客户满意度跟踪和分析
# 🏗️ 系统架构设计
graph TB
A[客户服务入口] --> B[售后服务网关]
B --> C[退换货服务]
B --> D[质量问题服务]
B --> E[投诉管理服务]
B --> F[保修服务]
C --> G[退货审核]
C --> H[换货处理]
C --> I[退款处理]
D --> J[质检报告]
D --> K[问题分类]
D --> L[解决方案]
E --> M[投诉分级]
E --> N[处理流程]
E --> O[满意度跟踪]
F --> P[保修验证]
F --> Q[维修安排]
F --> R[配件管理]
S[工单系统] --> T[任务分配]
S --> U[进度跟踪]
S --> V[绩效统计]
W[通知服务] --> X[邮件通知]
W --> Y[短信通知]
W --> Z[推送通知]
AA[数据分析] --> BB[趋势分析]
AA --> CC[问题统计]
AA --> DD[客户画像]
# 💻 核心代码实现
# 1. 售后服务订单实体模型
@Entity
@Table(name = "after_sales_orders")
public class AfterSalesOrder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "after_sales_number", unique = true, nullable = false)
private String afterSalesNumber;
@Column(name = "original_order_id", nullable = false)
private String originalOrderId;
@Column(name = "customer_id", nullable = false)
private Long customerId;
@Enumerated(EnumType.STRING)
@Column(name = "service_type")
private AfterSalesType serviceType;
@Enumerated(EnumType.STRING)
@Column(name = "status")
private AfterSalesStatus status;
@Enumerated(EnumType.STRING)
@Column(name = "reason")
private AfterSalesReason reason;
@Column(name = "description", length = 2000)
private String description;
@Column(name = "refund_amount", precision = 10, scale = 2)
private BigDecimal refundAmount;
@Column(name = "compensation_amount", precision = 10, scale = 2)
private BigDecimal compensationAmount;
@OneToMany(mappedBy = "afterSalesOrder", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<AfterSalesItem> items = new ArrayList<>();
@OneToMany(mappedBy = "afterSalesOrder", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<AfterSalesEvidence> evidences = new ArrayList<>();
@OneToMany(mappedBy = "afterSalesOrder", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<AfterSalesEvent> events = new ArrayList<>();
@Column(name = "assigned_agent_id")
private Long assignedAgentId;
@Column(name = "priority_level")
private Integer priorityLevel;
@Column(name = "expected_resolution_date")
private LocalDateTime expectedResolutionDate;
@Column(name = "actual_resolution_date")
private LocalDateTime actualResolutionDate;
@Column(name = "customer_satisfaction_score")
private Integer customerSatisfactionScore;
@CreationTimestamp
@Column(name = "created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// 构造函数
public AfterSalesOrder() {}
public AfterSalesOrder(String originalOrderId, Long customerId,
AfterSalesType serviceType, AfterSalesReason reason) {
this.afterSalesNumber = generateAfterSalesNumber();
this.originalOrderId = originalOrderId;
this.customerId = customerId;
this.serviceType = serviceType;
this.reason = reason;
this.status = AfterSalesStatus.PENDING;
this.priorityLevel = calculatePriority(serviceType, reason);
}
// 添加售后事件
public void addEvent(AfterSalesEventType eventType, String description, Long operatorId) {
AfterSalesEvent event = new AfterSalesEvent(
this, eventType, description, operatorId);
this.events.add(event);
}
// 更新状态
public void updateStatus(AfterSalesStatus newStatus, Long operatorId) {
AfterSalesStatus oldStatus = this.status;
this.status = newStatus;
addEvent(AfterSalesEventType.STATUS_CHANGE,
String.format("状态从 %s 变更为 %s", oldStatus, newStatus), operatorId);
if (newStatus == AfterSalesStatus.RESOLVED) {
this.actualResolutionDate = LocalDateTime.now();
}
}
// 计算优先级
private Integer calculatePriority(AfterSalesType serviceType, AfterSalesReason reason) {
// 质量问题和安全问题优先级最高
if (reason == AfterSalesReason.QUALITY_ISSUE ||
reason == AfterSalesReason.SAFETY_ISSUE) {
return 1; // 最高优先级
}
// 退货退款次之
if (serviceType == AfterSalesType.RETURN_REFUND) {
return 2;
}
// 其他情况
return 3;
}
// 生成售后单号
private String generateAfterSalesNumber() {
return "AS" + System.currentTimeMillis() +
String.format("%04d", new Random().nextInt(10000));
}
// getter和setter方法省略...
}
public enum AfterSalesType {
RETURN_REFUND, // 退货退款
EXCHANGE, // 换货
REPAIR, // 维修
COMPLAINT, // 投诉
CONSULTATION // 咨询
}
public enum AfterSalesStatus {
PENDING, // 待处理
IN_PROGRESS, // 处理中
WAITING_CUSTOMER, // 等待客户
RESOLVED, // 已解决
CLOSED, // 已关闭
ESCALATED // 已升级
}
public enum AfterSalesReason {
QUALITY_ISSUE, // 质量问题
SIZE_MISMATCH, // 尺寸不符
DESCRIPTION_MISMATCH, // 描述不符
DAMAGED_IN_TRANSIT, // 运输损坏
NOT_AS_EXPECTED, // 不符合预期
SAFETY_ISSUE, // 安全问题
DEFECTIVE, // 有缺陷
OTHER // 其他
}
# 2. 售后服务核心业务逻辑
@Service
@Slf4j
public class AfterSalesService {
@Autowired
private AfterSalesOrderRepository afterSalesOrderRepository;
@Autowired
private OrderService orderService;
@Autowired
private RefundService refundService;
@Autowired
private InventoryService inventoryService;
@Autowired
private NotificationService notificationService;
@Autowired
private WorkflowEngine workflowEngine;
/**
* 创建售后申请
*/
@Transactional
public AfterSalesOrder createAfterSalesRequest(AfterSalesRequest request) {
log.info("创建售后申请 - 订单: {}, 类型: {}",
request.getOriginalOrderId(), request.getServiceType());
// 1. 验证原订单
Order originalOrder = orderService.getOrderById(request.getOriginalOrderId());
validateOrderForAfterSales(originalOrder);
// 2. 创建售后订单
AfterSalesOrder afterSalesOrder = new AfterSalesOrder(
request.getOriginalOrderId(),
request.getCustomerId(),
request.getServiceType(),
request.getReason()
);
afterSalesOrder.setDescription(request.getDescription());
// 3. 添加售后商品
for (AfterSalesItemRequest itemRequest : request.getItems()) {
AfterSalesItem item = new AfterSalesItem(
afterSalesOrder,
itemRequest.getProductId(),
itemRequest.getQuantity(),
itemRequest.getUnitPrice()
);
afterSalesOrder.getItems().add(item);
}
// 4. 添加证据材料
for (String evidenceUrl : request.getEvidenceUrls()) {
AfterSalesEvidence evidence = new AfterSalesEvidence(
afterSalesOrder, evidenceUrl, EvidenceType.IMAGE);
afterSalesOrder.getEvidences().add(evidence);
}
// 5. 保存售后订单
afterSalesOrder = afterSalesOrderRepository.save(afterSalesOrder);
// 6. 启动工作流
workflowEngine.startAfterSalesWorkflow(afterSalesOrder.getId());
// 7. 发送通知
notificationService.sendAfterSalesCreatedNotification(afterSalesOrder);
log.info("售后申请创建成功 - 售后单号: {}", afterSalesOrder.getAfterSalesNumber());
return afterSalesOrder;
}
/**
* 处理退货退款
*/
@Transactional
public void processReturnRefund(Long afterSalesOrderId, ReturnRefundDecision decision) {
AfterSalesOrder afterSalesOrder = getAfterSalesOrderById(afterSalesOrderId);
if (afterSalesOrder.getServiceType() != AfterSalesType.RETURN_REFUND) {
throw new AfterSalesException("售后类型不匹配");
}
if (decision.isApproved()) {
// 批准退货退款
approveReturnRefund(afterSalesOrder, decision);
} else {
// 拒绝退货退款
rejectReturnRefund(afterSalesOrder, decision.getRejectionReason());
}
}
/**
* 批准退货退款
*/
private void approveReturnRefund(AfterSalesOrder afterSalesOrder,
ReturnRefundDecision decision) {
log.info("批准退货退款 - 售后单号: {}", afterSalesOrder.getAfterSalesNumber());
// 1. 更新状态
afterSalesOrder.updateStatus(AfterSalesStatus.IN_PROGRESS, decision.getOperatorId());
// 2. 计算退款金额
BigDecimal refundAmount = calculateRefundAmount(afterSalesOrder, decision);
afterSalesOrder.setRefundAmount(refundAmount);
// 3. 生成退货标签
String returnLabel = generateReturnLabel(afterSalesOrder);
// 4. 发送退货指引
notificationService.sendReturnInstructions(
afterSalesOrder.getCustomerId(), returnLabel);
// 5. 创建退款预处理
refundService.createPendingRefund(
afterSalesOrder.getOriginalOrderId(),
refundAmount,
"退货退款"
);
afterSalesOrderRepository.save(afterSalesOrder);
}
/**
* 处理换货申请
*/
@Transactional
public void processExchange(Long afterSalesOrderId, ExchangeDecision decision) {
AfterSalesOrder afterSalesOrder = getAfterSalesOrderById(afterSalesOrderId);
if (afterSalesOrder.getServiceType() != AfterSalesType.EXCHANGE) {
throw new AfterSalesException("售后类型不匹配");
}
if (decision.isApproved()) {
approveExchange(afterSalesOrder, decision);
} else {
rejectExchange(afterSalesOrder, decision.getRejectionReason());
}
}
/**
* 批准换货
*/
private void approveExchange(AfterSalesOrder afterSalesOrder, ExchangeDecision decision) {
log.info("批准换货 - 售后单号: {}", afterSalesOrder.getAfterSalesNumber());
// 1. 检查库存
for (AfterSalesItem item : afterSalesOrder.getItems()) {
boolean hasStock = inventoryService.checkStock(
decision.getNewProductId(), item.getQuantity());
if (!hasStock) {
throw new AfterSalesException("换货商品库存不足");
}
}
// 2. 预留库存
for (AfterSalesItem item : afterSalesOrder.getItems()) {
inventoryService.reserveStock(
decision.getNewProductId(), item.getQuantity());
}
// 3. 更新状态
afterSalesOrder.updateStatus(AfterSalesStatus.IN_PROGRESS, decision.getOperatorId());
// 4. 生成换货订单
String exchangeOrderId = createExchangeOrder(afterSalesOrder, decision);
// 5. 发送换货通知
notificationService.sendExchangeApprovedNotification(
afterSalesOrder.getCustomerId(), exchangeOrderId);
afterSalesOrderRepository.save(afterSalesOrder);
}
/**
* 处理质量问题
*/
@Transactional
public void processQualityIssue(Long afterSalesOrderId, QualityIssueDecision decision) {
AfterSalesOrder afterSalesOrder = getAfterSalesOrderById(afterSalesOrderId);
// 1. 记录质量问题
QualityIssueRecord qualityRecord = new QualityIssueRecord(
afterSalesOrder.getOriginalOrderId(),
afterSalesOrder.getReason(),
afterSalesOrder.getDescription(),
decision.getSeverityLevel()
);
// 2. 根据严重程度处理
switch (decision.getSeverityLevel()) {
case CRITICAL:
handleCriticalQualityIssue(afterSalesOrder, decision);
break;
case HIGH:
handleHighQualityIssue(afterSalesOrder, decision);
break;
case MEDIUM:
handleMediumQualityIssue(afterSalesOrder, decision);
break;
case LOW:
handleLowQualityIssue(afterSalesOrder, decision);
break;
}
// 3. 通知质量管理部门
notificationService.sendQualityIssueAlert(qualityRecord);
}
/**
* 处理严重质量问题
*/
private void handleCriticalQualityIssue(AfterSalesOrder afterSalesOrder,
QualityIssueDecision decision) {
// 1. 立即全额退款
BigDecimal fullRefundAmount = calculateFullRefundAmount(afterSalesOrder);
afterSalesOrder.setRefundAmount(fullRefundAmount);
// 2. 提供额外补偿
BigDecimal compensationAmount = fullRefundAmount.multiply(new BigDecimal("0.2"));
afterSalesOrder.setCompensationAmount(compensationAmount);
// 3. 启动产品召回流程
initiateProductRecall(afterSalesOrder);
// 4. 升级到高级管理层
escalateToManagement(afterSalesOrder, "严重质量问题");
afterSalesOrder.updateStatus(AfterSalesStatus.ESCALATED, decision.getOperatorId());
}
/**
* 自动分配客服代表
*/
public void autoAssignAgent(Long afterSalesOrderId) {
AfterSalesOrder afterSalesOrder = getAfterSalesOrderById(afterSalesOrderId);
// 根据优先级和客服工作负载分配
Long agentId = findBestAvailableAgent(
afterSalesOrder.getPriorityLevel(),
afterSalesOrder.getServiceType()
);
if (agentId != null) {
afterSalesOrder.setAssignedAgentId(agentId);
afterSalesOrder.addEvent(AfterSalesEventType.AGENT_ASSIGNED,
"系统自动分配客服代表: " + agentId, null);
afterSalesOrderRepository.save(afterSalesOrder);
// 通知客服代表
notificationService.sendAgentAssignmentNotification(agentId, afterSalesOrderId);
}
}
/**
* 查找最佳可用客服代表
*/
private Long findBestAvailableAgent(Integer priorityLevel, AfterSalesType serviceType) {
// 实现客服代表分配算法
// 考虑因素:工作负载、专业技能、在线状态等
return null; // 简化实现
}
// 其他辅助方法省略...
}
### 4. 客户满意度管理
```java
@Service
@Slf4j
public class CustomerSatisfactionService {
@Autowired
private SatisfactionSurveyRepository surveyRepository;
@Autowired
private AfterSalesOrderRepository afterSalesOrderRepository;
@Autowired
private NotificationService notificationService;
@Autowired
private AnalyticsService analyticsService;
/**
* 发送满意度调研
*/
public void sendSatisfactionSurvey(Long afterSalesOrderId) {
AfterSalesOrder afterSalesOrder = afterSalesOrderRepository
.findById(afterSalesOrderId)
.orElseThrow(() -> new AfterSalesException("售后订单不存在"));
if (afterSalesOrder.getStatus() != AfterSalesStatus.RESOLVED) {
throw new AfterSalesException("只能对已解决的售后订单发送满意度调研");
}
// 创建满意度调研
SatisfactionSurvey survey = new SatisfactionSurvey();
survey.setAfterSalesOrderId(afterSalesOrderId);
survey.setCustomerId(afterSalesOrder.getCustomerId());
survey.setSurveyToken(generateSurveyToken());
survey.setStatus(SurveyStatus.SENT);
survey.setExpiresAt(LocalDateTime.now().plusDays(7));
survey = surveyRepository.save(survey);
// 发送调研邮件
notificationService.sendSatisfactionSurveyEmail(
afterSalesOrder.getCustomerId(), survey.getSurveyToken());
log.info("已发送满意度调研 - 售后单号: {}, 调研ID: {}",
afterSalesOrder.getAfterSalesNumber(), survey.getId());
}
/**
* 提交满意度评价
*/
@Transactional
public void submitSatisfactionRating(String surveyToken, SatisfactionRatingRequest request) {
SatisfactionSurvey survey = surveyRepository.findBySurveyToken(surveyToken)
.orElseThrow(() -> new SurveyNotFoundException("调研不存在或已过期"));
if (survey.getStatus() != SurveyStatus.SENT) {
throw new SurveyException("调研状态不正确");
}
if (survey.getExpiresAt().isBefore(LocalDateTime.now())) {
throw new SurveyException("调研已过期");
}
// 更新调研结果
survey.setOverallRating(request.getOverallRating());
survey.setServiceQualityRating(request.getServiceQualityRating());
survey.setResponseTimeRating(request.getResponseTimeRating());
survey.setProfessionalismRating(request.getProfessionalismRating());
survey.setResolutionRating(request.getResolutionRating());
survey.setComments(request.getComments());
survey.setStatus(SurveyStatus.COMPLETED);
survey.setCompletedAt(LocalDateTime.now());
surveyRepository.save(survey);
// 更新售后订单的满意度评分
AfterSalesOrder afterSalesOrder = afterSalesOrderRepository
.findById(survey.getAfterSalesOrderId())
.orElseThrow(() -> new AfterSalesException("售后订单不存在"));
afterSalesOrder.setCustomerSatisfactionScore(request.getOverallRating());
afterSalesOrderRepository.save(afterSalesOrder);
// 如果评分较低,触发改进流程
if (request.getOverallRating() <= 3) {
triggerImprovementProcess(survey, afterSalesOrder);
}
// 更新客服代表的绩效
if (afterSalesOrder.getAssignedAgentId() != null) {
updateAgentPerformance(afterSalesOrder.getAssignedAgentId(), request);
}
log.info("满意度评价已提交 - 调研ID: {}, 总体评分: {}",
survey.getId(), request.getOverallRating());
}
/**
* 触发改进流程
*/
private void triggerImprovementProcess(SatisfactionSurvey survey, AfterSalesOrder afterSalesOrder) {
// 创建改进任务
ImprovementTask task = new ImprovementTask();
task.setAfterSalesOrderId(afterSalesOrder.getId());
task.setSurveyId(survey.getId());
task.setIssueDescription(survey.getComments());
task.setPriority(calculateImprovementPriority(survey.getOverallRating()));
task.setStatus(ImprovementStatus.PENDING);
// 分配给质量管理团队
task.setAssignedTeam("QUALITY_MANAGEMENT");
// 发送改进通知
notificationService.sendImprovementTaskNotification(task);
log.warn("触发改进流程 - 售后单号: {}, 满意度评分: {}",
afterSalesOrder.getAfterSalesNumber(), survey.getOverallRating());
}
/**
* 更新客服代表绩效
*/
private void updateAgentPerformance(Long agentId, SatisfactionRatingRequest rating) {
AgentPerformance performance = getOrCreateAgentPerformance(agentId);
// 更新满意度统计
performance.addSatisfactionRating(
rating.getOverallRating(),
rating.getServiceQualityRating(),
rating.getResponseTimeRating(),
rating.getProfessionalismRating(),
rating.getResolutionRating()
);
// 计算新的绩效评分
performance.calculatePerformanceScore();
// 如果绩效持续下降,触发培训计划
if (performance.getPerformanceScore() < 3.0) {
triggerTrainingPlan(agentId, performance);
}
}
/**
* 生成满意度报告
*/
public SatisfactionReport generateSatisfactionReport(LocalDate startDate, LocalDate endDate) {
List<SatisfactionSurvey> surveys = surveyRepository
.findCompletedSurveysBetween(startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
SatisfactionReport report = new SatisfactionReport();
report.setPeriod(startDate + " to " + endDate);
report.setTotalSurveys(surveys.size());
if (!surveys.isEmpty()) {
// 计算平均评分
double avgOverallRating = surveys.stream()
.mapToInt(SatisfactionSurvey::getOverallRating)
.average().orElse(0.0);
report.setAverageOverallRating(avgOverallRating);
double avgServiceQuality = surveys.stream()
.mapToInt(SatisfactionSurvey::getServiceQualityRating)
.average().orElse(0.0);
report.setAverageServiceQualityRating(avgServiceQuality);
double avgResponseTime = surveys.stream()
.mapToInt(SatisfactionSurvey::getResponseTimeRating)
.average().orElse(0.0);
report.setAverageResponseTimeRating(avgResponseTime);
// 计算满意度分布
Map<Integer, Long> ratingDistribution = surveys.stream()
.collect(Collectors.groupingBy(
SatisfactionSurvey::getOverallRating,
Collectors.counting()
));
report.setRatingDistribution(ratingDistribution);
// 分析评论关键词
List<String> keywordAnalysis = analyzeCommentKeywords(surveys);
report.setCommentKeywords(keywordAnalysis);
}
return report;
}
// 其他辅助方法省略...
}
# 5. 数据分析服务
@Service
public class AfterSalesAnalyticsService {
@Autowired
private AfterSalesOrderRepository afterSalesOrderRepository;
@Autowired
private SatisfactionSurveyRepository surveyRepository;
@Autowired
private TicketRepository ticketRepository;
/**
* 售后趋势分析
*/
public AfterSalesTrendAnalysis analyzeTrends(LocalDate startDate, LocalDate endDate) {
List<AfterSalesOrder> orders = afterSalesOrderRepository
.findByCreatedAtBetween(startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
AfterSalesTrendAnalysis analysis = new AfterSalesTrendAnalysis();
analysis.setPeriod(startDate + " to " + endDate);
// 按类型统计
Map<AfterSalesType, Long> typeDistribution = orders.stream()
.collect(Collectors.groupingBy(
AfterSalesOrder::getServiceType,
Collectors.counting()
));
analysis.setTypeDistribution(typeDistribution);
// 按原因统计
Map<AfterSalesReason, Long> reasonDistribution = orders.stream()
.collect(Collectors.groupingBy(
AfterSalesOrder::getReason,
Collectors.counting()
));
analysis.setReasonDistribution(reasonDistribution);
// 按状态统计
Map<AfterSalesStatus, Long> statusDistribution = orders.stream()
.collect(Collectors.groupingBy(
AfterSalesOrder::getStatus,
Collectors.counting()
));
analysis.setStatusDistribution(statusDistribution);
// 计算解决时间统计
List<Long> resolutionTimes = orders.stream()
.filter(order -> order.getActualResolutionDate() != null)
.map(order -> ChronoUnit.HOURS.between(
order.getCreatedAt(), order.getActualResolutionDate()))
.collect(Collectors.toList());
if (!resolutionTimes.isEmpty()) {
analysis.setAverageResolutionTimeHours(
resolutionTimes.stream().mapToLong(Long::longValue).average().orElse(0.0));
analysis.setMedianResolutionTimeHours(
calculateMedian(resolutionTimes));
}
return analysis;
}
/**
* 客服绩效分析
*/
public AgentPerformanceAnalysis analyzeAgentPerformance(Long agentId,
LocalDate startDate, LocalDate endDate) {
// 获取客服处理的售后订单
List<AfterSalesOrder> agentOrders = afterSalesOrderRepository
.findByAssignedAgentIdAndCreatedAtBetween(
agentId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
// 获取客服处理的工单
List<Ticket> agentTickets = ticketRepository
.findByAssignedAgentIdAndCreatedAtBetween(
agentId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
AgentPerformanceAnalysis analysis = new AgentPerformanceAnalysis();
analysis.setAgentId(agentId);
analysis.setPeriod(startDate + " to " + endDate);
// 处理数量统计
analysis.setTotalAfterSalesOrders(agentOrders.size());
analysis.setTotalTickets(agentTickets.size());
// 解决率统计
long resolvedOrders = agentOrders.stream()
.mapToLong(order -> order.getStatus() == AfterSalesStatus.RESOLVED ? 1 : 0)
.sum();
analysis.setResolutionRate(
agentOrders.isEmpty() ? 0.0 : (double) resolvedOrders / agentOrders.size() * 100);
// 平均处理时间
double avgResolutionTime = agentOrders.stream()
.filter(order -> order.getActualResolutionDate() != null)
.mapToLong(order -> ChronoUnit.HOURS.between(
order.getCreatedAt(), order.getActualResolutionDate()))
.average().orElse(0.0);
analysis.setAverageResolutionTimeHours(avgResolutionTime);
// 客户满意度统计
List<Integer> satisfactionScores = agentOrders.stream()
.filter(order -> order.getCustomerSatisfactionScore() != null)
.map(AfterSalesOrder::getCustomerSatisfactionScore)
.collect(Collectors.toList());
if (!satisfactionScores.isEmpty()) {
analysis.setAverageSatisfactionScore(
satisfactionScores.stream().mapToInt(Integer::intValue).average().orElse(0.0));
}
return analysis;
}
/**
* 产品质量分析
*/
public ProductQualityAnalysis analyzeProductQuality(String productId,
LocalDate startDate, LocalDate endDate) {
// 获取产品相关的售后订单
List<AfterSalesOrder> productOrders = afterSalesOrderRepository
.findByProductIdAndCreatedAtBetween(
productId, startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
ProductQualityAnalysis analysis = new ProductQualityAnalysis();
analysis.setProductId(productId);
analysis.setPeriod(startDate + " to " + endDate);
// 售后率计算(需要结合销售数据)
// 这里简化处理,实际需要从订单系统获取销售数据
analysis.setTotalAfterSalesCount(productOrders.size());
// 问题类型分布
Map<AfterSalesReason, Long> issueDistribution = productOrders.stream()
.collect(Collectors.groupingBy(
AfterSalesOrder::getReason,
Collectors.counting()
));
analysis.setIssueDistribution(issueDistribution);
// 质量问题严重程度分析
long qualityIssues = productOrders.stream()
.mapToLong(order ->
(order.getReason() == AfterSalesReason.QUALITY_ISSUE ||
order.getReason() == AfterSalesReason.DEFECTIVE) ? 1 : 0)
.sum();
analysis.setQualityIssueCount(qualityIssues);
// 客户满意度统计
List<Integer> satisfactionScores = productOrders.stream()
.filter(order -> order.getCustomerSatisfactionScore() != null)
.map(AfterSalesOrder::getCustomerSatisfactionScore)
.collect(Collectors.toList());
if (!satisfactionScores.isEmpty()) {
analysis.setAverageSatisfactionScore(
satisfactionScores.stream().mapToInt(Integer::intValue).average().orElse(0.0));
}
return analysis;
}
/**
* 成本效益分析
*/
public CostBenefitAnalysis analyzeCostBenefit(LocalDate startDate, LocalDate endDate) {
List<AfterSalesOrder> orders = afterSalesOrderRepository
.findByCreatedAtBetween(startDate.atStartOfDay(), endDate.atTime(23, 59, 59));
CostBenefitAnalysis analysis = new CostBenefitAnalysis();
analysis.setPeriod(startDate + " to " + endDate);
// 计算总成本
BigDecimal totalRefundAmount = orders.stream()
.filter(order -> order.getRefundAmount() != null)
.map(AfterSalesOrder::getRefundAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal totalCompensationAmount = orders.stream()
.filter(order -> order.getCompensationAmount() != null)
.map(AfterSalesOrder::getCompensationAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
analysis.setTotalRefundAmount(totalRefundAmount);
analysis.setTotalCompensationAmount(totalCompensationAmount);
analysis.setTotalCost(totalRefundAmount.add(totalCompensationAmount));
// 计算人力成本(简化计算)
long totalProcessingHours = orders.stream()
.filter(order -> order.getActualResolutionDate() != null)
.mapToLong(order -> ChronoUnit.HOURS.between(
order.getCreatedAt(), order.getActualResolutionDate()))
.sum();
BigDecimal laborCost = BigDecimal.valueOf(totalProcessingHours)
.multiply(new BigDecimal("50")); // 假设每小时成本50元
analysis.setLaborCost(laborCost);
// 计算客户保留价值(需要结合客户生命周期价值)
long retainedCustomers = calculateRetainedCustomers(orders);
analysis.setRetainedCustomers(retainedCustomers);
return analysis;
}
// 其他辅助方法省略...
}
# 🔧 技术亮点
# 1. 智能工单分配
- 负载均衡算法:基于客服工作负载和技能匹配的智能分配
- 优先级管理:多维度优先级计算和动态调整
- SLA监控:实时监控服务水平协议执行情况
# 2. 自动化处理流程
- 规则引擎:基于业务规则的自动化决策
- 工作流引擎:灵活的售后处理工作流配置
- 异常升级机制:自动识别和升级复杂问题
# 3. 客户满意度管理
- 多维度评价:全方位的服务质量评估
- 实时反馈:即时的客户满意度收集
- 改进闭环:基于反馈的持续改进机制
# ⚡ 性能优化
# 1. 缓存策略
@Configuration
@EnableCaching
public class AfterSalesCacheConfig {
@Bean
public CacheManager afterSalesCacheManager() {
RedisCacheManager.Builder builder = RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory())
.cacheDefaults(afterSalesCacheConfiguration());
return builder.build();
}
private RedisCacheConfiguration afterSalesCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(2))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
// 客服代表缓存配置
@Bean
public RedisCacheConfiguration agentCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.prefixCacheNameWith("after-sales:agent:");
}
// 工单缓存配置
@Bean
public RedisCacheConfiguration ticketCacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.prefixCacheNameWith("after-sales:ticket:");
}
}
# 2. 异步处理配置
@Configuration
@EnableAsync
public class AfterSalesAsyncConfig {
@Bean(name = "afterSalesTaskExecutor")
public TaskExecutor afterSalesTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(32);
executor.setQueueCapacity(150);
executor.setThreadNamePrefix("after-sales-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Bean(name = "notificationTaskExecutor")
public TaskExecutor notificationTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("notification-");
executor.initialize();
return executor;
}
}
# 📊 监控与分析
# 1. 售后服务监控指标
@Component
public class AfterSalesMetrics {
private final MeterRegistry meterRegistry;
private final Counter afterSalesCreatedCounter;
private final Counter afterSalesResolvedCounter;
private final Timer resolutionTimeTimer;
private final Gauge pendingOrdersGauge;
public AfterSalesMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.afterSalesCreatedCounter = Counter.builder("after_sales.orders.created")
.description("创建的售后订单数量")
.register(meterRegistry);
this.afterSalesResolvedCounter = Counter.builder("after_sales.orders.resolved")
.description("解决的售后订单数量")
.register(meterRegistry);
this.resolutionTimeTimer = Timer.builder("after_sales.resolution.time")
.description("售后问题解决时间")
.register(meterRegistry);
this.pendingOrdersGauge = Gauge.builder("after_sales.orders.pending")
.description("待处理售后订单数量")
.register(meterRegistry, this, AfterSalesMetrics::getPendingOrdersCount);
}
public void recordAfterSalesCreated(AfterSalesType type, AfterSalesReason reason) {
afterSalesCreatedCounter.increment(
Tags.of(
"type", type.name(),
"reason", reason.name()
)
);
}
public void recordAfterSalesResolved(AfterSalesType type, Duration resolutionTime) {
afterSalesResolvedCounter.increment(Tags.of("type", type.name()));
resolutionTimeTimer.record(resolutionTime, Tags.of("type", type.name()));
}
public void recordCustomerSatisfaction(int satisfactionScore, AfterSalesType type) {
Gauge.builder("after_sales.customer.satisfaction")
.description("客户满意度评分")
.tags("type", type.name())
.register(meterRegistry, satisfactionScore, score -> score);
}
private double getPendingOrdersCount() {
// 实现获取待处理订单数量的逻辑
return 0.0;
}
}
# 2. 实时监控面板
@RestController
@RequestMapping("/api/after-sales/dashboard")
public class AfterSalesDashboardController {
@Autowired
private AfterSalesAnalyticsService analyticsService;
@Autowired
private AfterSalesOrderRepository afterSalesOrderRepository;
@Autowired
private TicketRepository ticketRepository;
/**
* 获取实时监控数据
*/
@GetMapping("/realtime")
public RealtimeDashboard getRealtimeDashboard() {
RealtimeDashboard dashboard = new RealtimeDashboard();
// 今日统计
LocalDate today = LocalDate.now();
dashboard.setTodayCreated(afterSalesOrderRepository
.countByCreatedAtBetween(today.atStartOfDay(), today.atTime(23, 59, 59)));
dashboard.setTodayResolved(afterSalesOrderRepository
.countByStatusAndActualResolutionDateBetween(
AfterSalesStatus.RESOLVED, today.atStartOfDay(), today.atTime(23, 59, 59)));
// 待处理统计
dashboard.setPendingCount(afterSalesOrderRepository
.countByStatusIn(Arrays.asList(
AfterSalesStatus.PENDING,
AfterSalesStatus.IN_PROGRESS
)));
// 超时统计
dashboard.setOverdueCount(afterSalesOrderRepository
.countOverdueOrders(LocalDateTime.now()));
// 客服工作负载
dashboard.setAgentWorkload(getAgentWorkloadSummary());
return dashboard;
}
/**
* 获取趋势数据
*/
@GetMapping("/trends")
public TrendData getTrendData(@RequestParam int days) {
LocalDate endDate = LocalDate.now();
LocalDate startDate = endDate.minusDays(days);
return analyticsService.analyzeTrends(startDate, endDate);
}
// 其他监控接口省略...
}
# 📝 关键要点
# 技术特色
- 智能工单系统:基于AI的工单分配和优先级管理
- 全流程自动化:从问题识别到解决的端到端自动化
- 多维度质量管控:客户满意度、处理时效、解决质量全方位监控
- 数据驱动决策:基于大数据分析的服务优化和预测
- 个性化服务:根据客户历史和偏好提供定制化服务
# 业务价值
- 提升客户满意度:快速响应和专业处理提升客户体验
- 降低运营成本:自动化流程减少人工干预和处理时间
- 提高服务质量:标准化流程确保服务一致性
- 增强品牌信任:优质售后服务建立客户信任和忠诚度
# 未来发展
- AI智能客服:集成自然语言处理的智能客服机器人
- 预测性服务:基于机器学习的问题预测和主动服务
- 全渠道整合:统一的多渠道客户服务体验
- 区块链溯源:利用区块链技术实现服务过程的透明化
# 3. 工单管理系统
@Service
@Slf4j
public class TicketManagementService {
@Autowired
private TicketRepository ticketRepository;
@Autowired
private AgentRepository agentRepository;
@Autowired
private TicketWorkflowEngine ticketWorkflowEngine;
@Autowired
private NotificationService notificationService;
/**
* 创建工单
*/
@Transactional
public Ticket createTicket(TicketRequest request) {
log.info("创建工单 - 类型: {}, 优先级: {}",
request.getType(), request.getPriority());
Ticket ticket = new Ticket();
ticket.setTicketNumber(generateTicketNumber());
ticket.setType(request.getType());
ticket.setPriority(request.getPriority());
ticket.setSubject(request.getSubject());
ticket.setDescription(request.getDescription());
ticket.setCustomerId(request.getCustomerId());
ticket.setStatus(TicketStatus.OPEN);
// 设置SLA时间
ticket.setSlaDeadline(calculateSlaDeadline(request.getPriority()));
ticket = ticketRepository.save(ticket);
// 自动分配客服
autoAssignTicket(ticket);
// 启动工单工作流
ticketWorkflowEngine.startTicketWorkflow(ticket.getId());
return ticket;
}
/**
* 自动分配工单
*/
private void autoAssignTicket(Ticket ticket) {
// 1. 查找可用的客服代表
List<Agent> availableAgents = agentRepository.findAvailableAgents(
ticket.getType(), ticket.getPriority());
if (availableAgents.isEmpty()) {
log.warn("没有可用的客服代表处理工单: {}", ticket.getTicketNumber());
return;
}
// 2. 选择最佳客服代表
Agent bestAgent = selectBestAgent(availableAgents, ticket);
// 3. 分配工单
assignTicketToAgent(ticket, bestAgent);
}
/**
* 选择最佳客服代表
*/
private Agent selectBestAgent(List<Agent> agents, Ticket ticket) {
return agents.stream()
.min(Comparator.comparing(Agent::getCurrentWorkload)
.thenComparing(agent -> -agent.getSkillScore(ticket.getType()))
.thenComparing(Agent::getResponseTime))
.orElse(agents.get(0));
}
/**
* 分配工单给客服代表
*/
@Transactional
public void assignTicketToAgent(Ticket ticket, Agent agent) {
ticket.setAssignedAgentId(agent.getId());
ticket.setAssignedAt(LocalDateTime.now());
ticket.setStatus(TicketStatus.ASSIGNED);
// 增加客服工作负载
agent.incrementWorkload();
ticketRepository.save(ticket);
agentRepository.save(agent);
// 通知客服代表
notificationService.sendTicketAssignmentNotification(agent.getId(), ticket.getId());
log.info("工单 {} 已分配给客服代表 {}", ticket.getTicketNumber(), agent.getName());
}
/**
* 更新工单状态
*/
@Transactional
public void updateTicketStatus(Long ticketId, TicketStatus newStatus,
String comment, Long operatorId) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new TicketNotFoundException("工单不存在"));
TicketStatus oldStatus = ticket.getStatus();
ticket.setStatus(newStatus);
// 添加状态变更记录
TicketStatusHistory statusHistory = new TicketStatusHistory(
ticket, oldStatus, newStatus, comment, operatorId);
ticket.getStatusHistory().add(statusHistory);
// 处理特殊状态
switch (newStatus) {
case IN_PROGRESS:
ticket.setStartedAt(LocalDateTime.now());
break;
case RESOLVED:
ticket.setResolvedAt(LocalDateTime.now());
// 释放客服工作负载
releaseAgentWorkload(ticket.getAssignedAgentId());
break;
case CLOSED:
ticket.setClosedAt(LocalDateTime.now());
break;
}
ticketRepository.save(ticket);
// 发送状态更新通知
notificationService.sendTicketStatusUpdateNotification(ticket);
}
/**
* 工单升级处理
*/
@Transactional
public void escalateTicket(Long ticketId, EscalationReason reason, Long operatorId) {
Ticket ticket = ticketRepository.findById(ticketId)
.orElseThrow(() -> new TicketNotFoundException("工单不存在"));
// 提升优先级
TicketPriority newPriority = escalatePriority(ticket.getPriority());
ticket.setPriority(newPriority);
// 重新计算SLA
ticket.setSlaDeadline(calculateSlaDeadline(newPriority));
// 分配给高级客服
Agent seniorAgent = findSeniorAgent(ticket.getType());
if (seniorAgent != null) {
assignTicketToAgent(ticket, seniorAgent);
}
// 记录升级事件
TicketEscalation escalation = new TicketEscalation(
ticket, reason, operatorId, LocalDateTime.now());
ticket.getEscalations().add(escalation);
ticketRepository.save(ticket);
// 通知管理层
notificationService.sendEscalationNotification(ticket, reason);
log.warn("工单 {} 已升级,原因: {}", ticket.getTicketNumber(), reason);
}
/**
* SLA监控
*/
@Scheduled(fixedDelay = 300000) // 每5分钟检查一次
public void monitorSlaViolations() {
LocalDateTime now = LocalDateTime.now();
// 查找即将违反SLA的工单
List<Ticket> nearViolationTickets = ticketRepository
.findTicketsNearSlaViolation(now.plusHours(1));
for (Ticket ticket : nearViolationTickets) {
// 发送SLA预警
notificationService.sendSlaWarningNotification(ticket);
}
// 查找已违反SLA的工单
List<Ticket> violatedTickets = ticketRepository
.findSlaViolatedTickets(now);
for (Ticket ticket : violatedTickets) {
// 自动升级
escalateTicket(ticket.getId(), EscalationReason.SLA_VIOLATION, null);
}
}
// 其他辅助方法省略...
}