feat: add daily summary feature and customer stats calibration
This commit adds multiple major features and improvements: 1. Adds Milvus vector database configuration and client integration 2. Implements daily summary query API for customer chat records 3. Adds customer statistics calibration functions, including single and full recalculation 4. Optimizes session list merging logic to handle bidirectional session IDs 5. Adds new mapper methods for customer message statistics query 6. Fixes message statistics update logic to avoid duplicate counting 7. Adds daily scheduled task to reset customer daily message count 8. Adds frontend page for viewing customer daily summaries
This commit is contained in:
parent
3aea6d4d9a
commit
d55ae221c2
@ -18,6 +18,7 @@
|
|||||||
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
|
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
|
||||||
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcpkix-jdk18on</artifactId><version>1.78</version></dependency>
|
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcpkix-jdk18on</artifactId><version>1.78</version></dependency>
|
||||||
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.4</version></dependency>
|
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.3.4</version></dependency>
|
||||||
|
<dependency><groupId>io.milvus</groupId><artifactId>milvus-sdk-java</artifactId><version>2.4.10</version></dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
package com.artedu.archive.config;
|
||||||
|
|
||||||
|
import io.milvus.client.MilvusServiceClient;
|
||||||
|
import io.milvus.param.ConnectParam;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Milvus 客户端配置
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
@EnableConfigurationProperties(MilvusProperties.class)
|
||||||
|
public class MilvusConfig {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MilvusProperties milvusProperties;
|
||||||
|
|
||||||
|
private MilvusServiceClient milvusClient;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public MilvusServiceClient milvusServiceClient() {
|
||||||
|
ConnectParam.Builder builder = ConnectParam.newBuilder()
|
||||||
|
.withHost(milvusProperties.getHost())
|
||||||
|
.withPort(milvusProperties.getPort());
|
||||||
|
|
||||||
|
if (milvusProperties.getUsername() != null && !milvusProperties.getUsername().isEmpty()) {
|
||||||
|
builder.withAuthorization(milvusProperties.getUsername(), milvusProperties.getPassword());
|
||||||
|
}
|
||||||
|
|
||||||
|
milvusClient = new MilvusServiceClient(builder.build());
|
||||||
|
log.info("Milvus client initialized: {}:{}, collection={}",
|
||||||
|
milvusProperties.getHost(), milvusProperties.getPort(), milvusProperties.getCollection());
|
||||||
|
return milvusClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void close() {
|
||||||
|
if (milvusClient != null) {
|
||||||
|
milvusClient.close();
|
||||||
|
log.info("Milvus client closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.artedu.archive.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Milvus 连接配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@ConfigurationProperties(prefix = "milvus")
|
||||||
|
public class MilvusProperties {
|
||||||
|
|
||||||
|
private String host = "localhost";
|
||||||
|
private Integer port = 19530;
|
||||||
|
private String collection = "message_summary";
|
||||||
|
private String username = "";
|
||||||
|
private String password = "";
|
||||||
|
}
|
||||||
@ -118,4 +118,19 @@ public class ArchiveContactController {
|
|||||||
int count = archiveContactService.fillAllCustomerDetails();
|
int count = archiveContactService.fillAllCustomerDetails();
|
||||||
return Result.success("已开始批量补充客户资料,共 " + count + " 人,请稍后在客户列表查看结果");
|
return Result.success("已开始批量补充客户资料,共 " + count + " 人,请稍后在客户列表查看结果");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 统计校准 ==========
|
||||||
|
|
||||||
|
@PostMapping("/customers/{customerId}/recalculate-stats")
|
||||||
|
public Result<String> recalculateCustomerStats(@PathVariable String customerId) {
|
||||||
|
boolean ok = archiveContactService.recalculateCustomerStats(customerId);
|
||||||
|
return ok ? Result.success("客户统计校准完成") : Result.fail("客户不存在或校准失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/customers/recalculate-all-stats")
|
||||||
|
public Result<String> recalculateAllCustomerStats(
|
||||||
|
@RequestParam(required = false, defaultValue = "100") @Min(1) Integer pageSize) {
|
||||||
|
int count = archiveContactService.recalculateAllCustomerStats(pageSize);
|
||||||
|
return Result.success("已开始全量校准客户统计,预计处理 " + count + " 人");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
package com.artedu.archive.controller;
|
||||||
|
|
||||||
|
import com.artedu.archive.service.DailySummaryService;
|
||||||
|
import com.artedu.archive.vo.DailySummaryVO;
|
||||||
|
import com.artedu.common.result.Result;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每日总结接口
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/archive")
|
||||||
|
public class DailySummaryController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DailySummaryService dailySummaryService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询客户指定日期的消息总结
|
||||||
|
*
|
||||||
|
* @param customerId 客户 ID(企微外部联系人 userid)
|
||||||
|
* @param date 日期 yyyy-MM-dd,默认今天
|
||||||
|
*/
|
||||||
|
@GetMapping("/daily-summary")
|
||||||
|
public Result<List<DailySummaryVO>> dailySummary(
|
||||||
|
@RequestParam String customerId,
|
||||||
|
@RequestParam(required = false) String date) {
|
||||||
|
|
||||||
|
List<DailySummaryVO> list = dailySummaryService.queryDailySummary(customerId, date);
|
||||||
|
return Result.success(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -78,6 +78,19 @@ public interface ArchiveMessageMapper extends BaseMapper<ArchiveMessage> {
|
|||||||
@Select("SELECT MAX(msgtime) FROM archive_messages WHERE corp_id = #{corpId} AND (from_user = #{userId} OR to_user = #{userId})")
|
@Select("SELECT MAX(msgtime) FROM archive_messages WHERE corp_id = #{corpId} AND (from_user = #{userId} OR to_user = #{userId})")
|
||||||
Long selectLastMsgTimeByAnyUser(@Param("userId") String userId, @Param("corpId") String corpId);
|
Long selectLastMsgTimeByAnyUser(@Param("userId") String userId, @Param("corpId") String corpId);
|
||||||
|
|
||||||
|
@Select("SELECT MIN(msgtime) FROM archive_messages WHERE corp_id = #{corpId} AND (from_user = #{userId} OR to_user = #{userId})")
|
||||||
|
Long selectFirstMsgTimeByAnyUser(@Param("userId") String userId, @Param("corpId") String corpId);
|
||||||
|
|
||||||
|
@Select("SELECT COUNT(*) FROM archive_messages WHERE corp_id = #{corpId} AND (from_user = #{userId} OR to_user = #{userId})")
|
||||||
|
Integer countMsgByAnyUser(@Param("userId") String userId, @Param("corpId") String corpId);
|
||||||
|
|
||||||
|
@Select("SELECT DISTINCT u.staff_id FROM (" +
|
||||||
|
" SELECT from_user AS staff_id FROM archive_messages WHERE corp_id = #{corpId} AND from_role = 'INTERNAL' AND (to_user = #{customerId} OR tolist LIKE CONCAT('%', #{customerId}, '%'))" +
|
||||||
|
" UNION" +
|
||||||
|
" SELECT to_user AS staff_id FROM archive_messages WHERE corp_id = #{corpId} AND from_user = #{customerId} AND to_user IS NOT NULL AND to_user != ''" +
|
||||||
|
") u WHERE u.staff_id IS NOT NULL AND u.staff_id != ''")
|
||||||
|
List<String> selectDistinctStaffIdsByCustomer(@Param("customerId") String customerId, @Param("corpId") String corpId);
|
||||||
|
|
||||||
@Select("SELECT COUNT(DISTINCT from_user) FROM archive_messages " +
|
@Select("SELECT COUNT(DISTINCT from_user) FROM archive_messages " +
|
||||||
"WHERE corp_id = #{corpId} AND from_role = 'EXTERNAL' " +
|
"WHERE corp_id = #{corpId} AND from_role = 'EXTERNAL' " +
|
||||||
"AND session_id IN (" +
|
"AND session_id IN (" +
|
||||||
|
|||||||
@ -9,10 +9,13 @@ import com.artedu.archive.mapper.CustomerMapper;
|
|||||||
import com.artedu.archive.mapper.StaffMapper;
|
import com.artedu.archive.mapper.StaffMapper;
|
||||||
import com.artedu.common.util.JsonUtils;
|
import com.artedu.common.util.JsonUtils;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@ -492,4 +495,111 @@ public class ArchiveContactService {
|
|||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每天凌晨 00:05 清零客户今日消息数
|
||||||
|
*/
|
||||||
|
@Scheduled(cron = "0 5 0 * * ?")
|
||||||
|
public void resetTodayMsgCount() {
|
||||||
|
try {
|
||||||
|
LambdaUpdateWrapper<Customer> wrapper = new LambdaUpdateWrapper<>();
|
||||||
|
wrapper.eq(Customer::getCorpId, corpId).set(Customer::getTodayMsgCount, 0);
|
||||||
|
int rows = customerMapper.update(null, wrapper);
|
||||||
|
log.info("清零客户今日消息数完成,共 {} 人", rows);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("清零客户今日消息数失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新计算单个客户的统计数据
|
||||||
|
*/
|
||||||
|
public boolean recalculateCustomerStats(String customerId) {
|
||||||
|
try {
|
||||||
|
Customer customer = customerMapper.selectByCustomerId(customerId, corpId);
|
||||||
|
if (customer == null) {
|
||||||
|
log.warn("客户不存在: {}", customerId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer totalCount = archiveMessageMapper.countMsgByAnyUser(customerId, corpId);
|
||||||
|
Integer todayCount = archiveMessageMapper.countTodayMsgByCustomer(customerId, corpId);
|
||||||
|
Long firstMsgTime = archiveMessageMapper.selectFirstMsgTimeByAnyUser(customerId, corpId);
|
||||||
|
Long lastMsgTime = archiveMessageMapper.selectLastMsgTimeByAnyUser(customerId, corpId);
|
||||||
|
List<String> rawStaffIds = archiveMessageMapper.selectDistinctStaffIdsByCustomer(customerId, corpId);
|
||||||
|
|
||||||
|
Set<String> staffIdSet = new HashSet<>();
|
||||||
|
if (rawStaffIds != null) {
|
||||||
|
for (String rawId : rawStaffIds) {
|
||||||
|
if (rawId == null || rawId.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 兼容历史数据中 to_user 存了 JSON 数组的情况,如 ["DengHan"]
|
||||||
|
if (rawId.startsWith("[")) {
|
||||||
|
try {
|
||||||
|
List<String> parsed = JsonUtils.fromJsonList(rawId, String.class);
|
||||||
|
if (parsed != null) {
|
||||||
|
staffIdSet.addAll(parsed.stream().filter(s -> s != null && !s.isEmpty()).collect(Collectors.toList()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("解析员工ID JSON 数组失败: {}", rawId);
|
||||||
|
}
|
||||||
|
staffIdSet.add(rawId.replaceAll("[\\[\\]\"]", "").trim());
|
||||||
|
} else {
|
||||||
|
staffIdSet.add(rawId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customer.setMsgCount(totalCount != null ? totalCount : 0);
|
||||||
|
customer.setTodayMsgCount(todayCount != null ? todayCount : 0);
|
||||||
|
customer.setFirstMsgTime(firstMsgTime);
|
||||||
|
customer.setLastMsgTime(lastMsgTime);
|
||||||
|
customer.setStaffIds(JsonUtils.toJson(new ArrayList<>(staffIdSet)));
|
||||||
|
|
||||||
|
customerMapper.updateById(customer);
|
||||||
|
log.info("重新计算客户统计完成: customerId={}, msgCount={}, todayMsgCount={}, staffCount={}",
|
||||||
|
customerId, customer.getMsgCount(), customer.getTodayMsgCount(), staffIdSet.size());
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("重新计算客户统计失败: customerId={}", customerId, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全量重新计算所有客户统计数据
|
||||||
|
*/
|
||||||
|
public int recalculateAllCustomerStats(int pageSize) {
|
||||||
|
int total = 0;
|
||||||
|
int success = 0;
|
||||||
|
try {
|
||||||
|
long pages = 0;
|
||||||
|
long current = 1;
|
||||||
|
do {
|
||||||
|
Page<Customer> page = customerMapper.selectPage(
|
||||||
|
new Page<>(current, pageSize),
|
||||||
|
new LambdaQueryWrapper<Customer>().eq(Customer::getCorpId, corpId)
|
||||||
|
);
|
||||||
|
List<Customer> customers = page.getRecords();
|
||||||
|
if (customers.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pages = page.getPages();
|
||||||
|
for (Customer customer : customers) {
|
||||||
|
total++;
|
||||||
|
if (recalculateCustomerStats(customer.getCustomerId())) {
|
||||||
|
success++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current++;
|
||||||
|
} while (current <= pages);
|
||||||
|
|
||||||
|
log.info("全量重新计算客户统计完成,共 {} 人,成功 {} 人", total, success);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("全量重新计算客户统计失败", e);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -596,14 +596,19 @@ public class ArchivePullService {
|
|||||||
for (ArchiveMessage message : messages) {
|
for (ArchiveMessage message : messages) {
|
||||||
try {
|
try {
|
||||||
int count = archiveMessageMapper.countByMsgId(message.getMsgid(), message.getCorpId());
|
int count = archiveMessageMapper.countByMsgId(message.getMsgid(), message.getCorpId());
|
||||||
|
boolean isNew = false;
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
int updated = archiveMessageMapper.updateByMsgId(message);
|
int updated = archiveMessageMapper.updateByMsgId(message);
|
||||||
if (updated > 0) { totalUpdated++; roundUpdated++; }
|
if (updated > 0) { totalUpdated++; roundUpdated++; }
|
||||||
} else {
|
} else {
|
||||||
archiveMessageMapper.insert(message);
|
archiveMessageMapper.insert(message);
|
||||||
totalInserted++; roundInserted++;
|
totalInserted++; roundInserted++;
|
||||||
|
isNew = true;
|
||||||
|
}
|
||||||
|
// 只有新消息才更新联系人统计,避免历史修复重复累加
|
||||||
|
if (isNew) {
|
||||||
|
archiveContactService.processMessage(message);
|
||||||
}
|
}
|
||||||
archiveContactService.processMessage(message);
|
|
||||||
archiveRiskService.processMessageRisk(message);
|
archiveRiskService.processMessageRisk(message);
|
||||||
redisTemplate.opsForValue().set(
|
redisTemplate.opsForValue().set(
|
||||||
LAST_SEQ_KEY + corpId,
|
LAST_SEQ_KEY + corpId,
|
||||||
|
|||||||
@ -96,23 +96,9 @@ public class ArchiveSessionService {
|
|||||||
public PageResult<SessionVO> listStaffSessions(String staffId, int page, int size) {
|
public PageResult<SessionVO> listStaffSessions(String staffId, int page, int size) {
|
||||||
int offset = (page - 1) * size;
|
int offset = (page - 1) * size;
|
||||||
List<Map<String, Object>> rows = archiveMessageMapper.selectSessionsByUser(staffId, corpId, offset, size);
|
List<Map<String, Object>> rows = archiveMessageMapper.selectSessionsByUser(staffId, corpId, offset, size);
|
||||||
Long total = archiveMessageMapper.countSessionsByUser(staffId, corpId);
|
|
||||||
|
|
||||||
List<SessionVO> list = new ArrayList<>();
|
List<SessionVO> list = mergeSessions(rows);
|
||||||
for (Map<String, Object> row : rows) {
|
return PageResult.of(page, size, (long) list.size(), list);
|
||||||
SessionVO vo = new SessionVO();
|
|
||||||
String sessionId = (String) row.get("session_id");
|
|
||||||
vo.setSessionId(sessionId);
|
|
||||||
vo.setMsgCount(((Number) row.get("cnt")).intValue());
|
|
||||||
Long lastTime = row.get("last_time") instanceof Number ? ((Number) row.get("last_time")).longValue() : null;
|
|
||||||
vo.setLastMsgTime(lastTime);
|
|
||||||
vo.setLastMsgTimeStr(formatTime(lastTime));
|
|
||||||
|
|
||||||
// 查询会话中的对方信息
|
|
||||||
fillSessionParticipants(vo, sessionId);
|
|
||||||
list.add(vo);
|
|
||||||
}
|
|
||||||
return PageResult.of(page, size, total != null ? total : 0L, list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -121,21 +107,71 @@ public class ArchiveSessionService {
|
|||||||
public PageResult<SessionVO> listCustomerSessions(String customerId, int page, int size) {
|
public PageResult<SessionVO> listCustomerSessions(String customerId, int page, int size) {
|
||||||
int offset = (page - 1) * size;
|
int offset = (page - 1) * size;
|
||||||
List<Map<String, Object>> rows = archiveMessageMapper.selectSessionsByUser(customerId, corpId, offset, size);
|
List<Map<String, Object>> rows = archiveMessageMapper.selectSessionsByUser(customerId, corpId, offset, size);
|
||||||
Long total = archiveMessageMapper.countSessionsByUser(customerId, corpId);
|
|
||||||
|
|
||||||
List<SessionVO> list = new ArrayList<>();
|
List<SessionVO> list = mergeSessions(rows);
|
||||||
|
return PageResult.of(page, size, (long) list.size(), list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并同一对参与者的双向 session_id
|
||||||
|
*/
|
||||||
|
private List<SessionVO> mergeSessions(List<Map<String, Object>> rows) {
|
||||||
|
Map<String, SessionVO> merged = new HashMap<>();
|
||||||
for (Map<String, Object> row : rows) {
|
for (Map<String, Object> row : rows) {
|
||||||
SessionVO vo = new SessionVO();
|
|
||||||
String sessionId = (String) row.get("session_id");
|
String sessionId = (String) row.get("session_id");
|
||||||
vo.setSessionId(sessionId);
|
String canonicalId = normalizeSessionId(sessionId);
|
||||||
vo.setMsgCount(((Number) row.get("cnt")).intValue());
|
SessionVO vo = merged.computeIfAbsent(canonicalId, k -> {
|
||||||
|
SessionVO s = new SessionVO();
|
||||||
|
s.setSessionId(canonicalId);
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
|
||||||
|
int cnt = ((Number) row.get("cnt")).intValue();
|
||||||
|
vo.setMsgCount((vo.getMsgCount() != null ? vo.getMsgCount() : 0) + cnt);
|
||||||
|
|
||||||
Long lastTime = row.get("last_time") instanceof Number ? ((Number) row.get("last_time")).longValue() : null;
|
Long lastTime = row.get("last_time") instanceof Number ? ((Number) row.get("last_time")).longValue() : null;
|
||||||
vo.setLastMsgTime(lastTime);
|
if (lastTime != null && (vo.getLastMsgTime() == null || lastTime > vo.getLastMsgTime())) {
|
||||||
vo.setLastMsgTimeStr(formatTime(lastTime));
|
vo.setLastMsgTime(lastTime);
|
||||||
fillSessionParticipants(vo, sessionId);
|
vo.setLastMsgTimeStr(formatTime(lastTime));
|
||||||
list.add(vo);
|
}
|
||||||
}
|
}
|
||||||
return PageResult.of(page, size, total != null ? total : 0L, list);
|
|
||||||
|
List<SessionVO> list = new ArrayList<>(merged.values());
|
||||||
|
for (SessionVO vo : list) {
|
||||||
|
fillSessionParticipants(vo, vo.getSessionId());
|
||||||
|
}
|
||||||
|
|
||||||
|
list.sort((a, b) -> Long.compare(
|
||||||
|
b.getLastMsgTime() != null ? b.getLastMsgTime() : 0L,
|
||||||
|
a.getLastMsgTime() != null ? a.getLastMsgTime() : 0L));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化单聊 session_id:按参与者字典序排序
|
||||||
|
*/
|
||||||
|
private String normalizeSessionId(String sessionId) {
|
||||||
|
if (sessionId == null || sessionId.isEmpty()) {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
int firstUnderscore = sessionId.indexOf('_');
|
||||||
|
if (firstUnderscore < 0) {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
String corpPrefix = sessionId.substring(0, firstUnderscore);
|
||||||
|
String rest = sessionId.substring(firstUnderscore + 1);
|
||||||
|
// 群聊:corpId_roomId,不处理
|
||||||
|
if (!rest.contains("_")) {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
String[] users = rest.split("_", 2);
|
||||||
|
if (users.length != 2) {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
if (users[0].compareTo(users[1]) > 0) {
|
||||||
|
return corpPrefix + "_" + users[1] + "_" + users[0];
|
||||||
|
}
|
||||||
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -0,0 +1,224 @@
|
|||||||
|
package com.artedu.archive.service;
|
||||||
|
|
||||||
|
import com.artedu.archive.config.MilvusProperties;
|
||||||
|
import com.artedu.archive.entity.Staff;
|
||||||
|
import com.artedu.archive.mapper.StaffMapper;
|
||||||
|
import com.artedu.archive.vo.DailySummaryVO;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import io.milvus.client.MilvusServiceClient;
|
||||||
|
import io.milvus.grpc.QueryResults;
|
||||||
|
import io.milvus.param.R;
|
||||||
|
import io.milvus.param.dml.QueryParam;
|
||||||
|
import io.milvus.response.FieldDataWrapper;
|
||||||
|
import io.milvus.response.QueryResultsWrapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每日总结查询服务
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class DailySummaryService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
private static final List<String> OUTPUT_FIELDS = Arrays.asList(
|
||||||
|
"id", "msg_time", "msg_type", "from_user", "to_user",
|
||||||
|
"room_id", "message_context", "summary", "created_at"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Value("${wecom.archive.corp-id}")
|
||||||
|
private String corpId;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MilvusServiceClient milvusClient;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MilvusProperties milvusProperties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StaffMapper staffMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询客户指定日期的消息总结
|
||||||
|
*
|
||||||
|
* @param customerId 客户企微外部联系人 userid
|
||||||
|
* @param date 日期 yyyy-MM-dd,为空则取当天
|
||||||
|
* @return 总结列表,按 created_at 升序
|
||||||
|
*/
|
||||||
|
public List<DailySummaryVO> queryDailySummary(String customerId, String date) {
|
||||||
|
if (customerId == null || customerId.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("customerId 不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String queryDate = (date == null || date.trim().isEmpty())
|
||||||
|
? LocalDate.now().format(DATE_FORMATTER)
|
||||||
|
: date.trim();
|
||||||
|
|
||||||
|
String expr = String.format(
|
||||||
|
"(from_user == '%s' || to_user == '%s') && msg_time == '%s'",
|
||||||
|
escape(customerId), escape(customerId), escape(queryDate)
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info("Query Milvus daily summary, customerId={}, date={}, expr={}", customerId, queryDate, expr);
|
||||||
|
|
||||||
|
QueryParam queryParam = QueryParam.newBuilder()
|
||||||
|
.withCollectionName(milvusProperties.getCollection())
|
||||||
|
.withExpr(expr)
|
||||||
|
.withOutFields(OUTPUT_FIELDS)
|
||||||
|
.withLimit(1000L)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
R<QueryResults> response = milvusClient.query(queryParam);
|
||||||
|
if (response.getStatus() != R.Status.Success.getCode()) {
|
||||||
|
String errorMsg = response.getException() != null ? response.getException().getMessage() : "未知错误";
|
||||||
|
log.error("Milvus query failed: status={}, message={}", response.getStatus(), errorMsg);
|
||||||
|
throw new RuntimeException("查询 Milvus 失败: " + errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DailySummaryVO> result = convert(response.getData());
|
||||||
|
fillStaffInfo(result, customerId);
|
||||||
|
|
||||||
|
result.sort((a, b) -> {
|
||||||
|
long ca = a.getCreatedAt() != null ? a.getCreatedAt() : 0L;
|
||||||
|
long cb = b.getCreatedAt() != null ? b.getCreatedAt() : 0L;
|
||||||
|
return Long.compare(ca, cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Milvus daily summary found {} records", result.size());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DailySummaryVO> convert(QueryResults queryResults) {
|
||||||
|
List<DailySummaryVO> list = new ArrayList<>();
|
||||||
|
if (queryResults == null) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryResultsWrapper wrapper = new QueryResultsWrapper(queryResults);
|
||||||
|
List<Long> ids = getField(wrapper, "id");
|
||||||
|
List<String> msgTimes = getField(wrapper, "msg_time");
|
||||||
|
List<Integer> msgTypes = getField(wrapper, "msg_type");
|
||||||
|
List<String> fromUsers = getField(wrapper, "from_user");
|
||||||
|
List<String> toUsers = getField(wrapper, "to_user");
|
||||||
|
List<String> roomIds = getField(wrapper, "room_id");
|
||||||
|
List<String> messageContexts = getField(wrapper, "message_context");
|
||||||
|
List<String> summaries = getField(wrapper, "summary");
|
||||||
|
List<Long> createdAts = getField(wrapper, "created_at");
|
||||||
|
|
||||||
|
int size = ids.size();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
DailySummaryVO vo = new DailySummaryVO();
|
||||||
|
vo.setId(getOrDefault(ids, i));
|
||||||
|
vo.setMsgTime(getOrDefault(msgTimes, i));
|
||||||
|
vo.setMsgType(getOrDefault(msgTypes, i));
|
||||||
|
vo.setFromUser(getOrDefault(fromUsers, i));
|
||||||
|
vo.setToUser(getOrDefault(toUsers, i));
|
||||||
|
vo.setRoomId(getOrDefault(roomIds, i));
|
||||||
|
vo.setMessageContext(getOrDefault(messageContexts, i));
|
||||||
|
vo.setSummary(getOrDefault(summaries, i));
|
||||||
|
vo.setCreatedAt(getOrDefault(createdAts, i));
|
||||||
|
list.add(vo);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为每条总结填充销售老师信息
|
||||||
|
*/
|
||||||
|
private void fillStaffInfo(List<DailySummaryVO> list, String customerId) {
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> staffIds = new HashSet<>();
|
||||||
|
for (DailySummaryVO vo : list) {
|
||||||
|
String staffId = resolveStaffId(vo, customerId);
|
||||||
|
if (staffId != null && !isExternalUserId(staffId)) {
|
||||||
|
staffIds.add(staffId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> staffNameMap = new HashMap<>();
|
||||||
|
if (!staffIds.isEmpty()) {
|
||||||
|
LambdaQueryWrapper<Staff> wrapper = new LambdaQueryWrapper<>();
|
||||||
|
wrapper.eq(Staff::getCorpId, corpId).in(Staff::getStaffId, staffIds);
|
||||||
|
List<Staff> staffs = staffMapper.selectList(wrapper);
|
||||||
|
staffNameMap = staffs.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
Staff::getStaffId,
|
||||||
|
s -> s.getName() != null && !s.getName().isEmpty() ? s.getName() : s.getStaffId(),
|
||||||
|
(a, b) -> a
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DailySummaryVO vo : list) {
|
||||||
|
String staffId = resolveStaffId(vo, customerId);
|
||||||
|
vo.setStaffId(staffId);
|
||||||
|
vo.setStaffName(staffNameMap.getOrDefault(staffId, staffId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 from_user / to_user 中找出销售老师 ID
|
||||||
|
*/
|
||||||
|
private String resolveStaffId(DailySummaryVO vo, String customerId) {
|
||||||
|
if (customerId.equals(vo.getFromUser())) {
|
||||||
|
return vo.getToUser();
|
||||||
|
}
|
||||||
|
if (customerId.equals(vo.getToUser())) {
|
||||||
|
return vo.getFromUser();
|
||||||
|
}
|
||||||
|
// 兜底:返回非客户 ID
|
||||||
|
if (!isExternalUserId(vo.getFromUser())) {
|
||||||
|
return vo.getFromUser();
|
||||||
|
}
|
||||||
|
return vo.getToUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isExternalUserId(String userId) {
|
||||||
|
return userId != null && (userId.startsWith("wm") || userId.startsWith("wo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private <T> List<T> getField(QueryResultsWrapper wrapper, String fieldName) {
|
||||||
|
try {
|
||||||
|
FieldDataWrapper field = wrapper.getFieldWrapper(fieldName);
|
||||||
|
if (field == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return (List<T>) field.getFieldData();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Parse Milvus field {} failed: {}", fieldName, e.getMessage());
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T getOrDefault(List<T> list, int index) {
|
||||||
|
if (list == null || index < 0 || index >= list.size()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return list.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escape(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return value.replace("'", "\\'");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package com.artedu.archive.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每日总结记录 VO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class DailySummaryVO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String msgTime;
|
||||||
|
private Integer msgType;
|
||||||
|
private String fromUser;
|
||||||
|
private String toUser;
|
||||||
|
private String roomId;
|
||||||
|
private String messageContext;
|
||||||
|
private String summary;
|
||||||
|
private Long createdAt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销售老师企微 userId
|
||||||
|
*/
|
||||||
|
private String staffId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 销售老师名称
|
||||||
|
*/
|
||||||
|
private String staffName;
|
||||||
|
}
|
||||||
@ -46,6 +46,13 @@ wecom:
|
|||||||
sdk-path: ${WECOM_ARCHIVE_SDK_PATH:}
|
sdk-path: ${WECOM_ARCHIVE_SDK_PATH:}
|
||||||
mock-mode: ${WECOM_ARCHIVE_MOCK:false}
|
mock-mode: ${WECOM_ARCHIVE_MOCK:false}
|
||||||
|
|
||||||
|
milvus:
|
||||||
|
host: ${MILVUS_HOST:8.159.132.53}
|
||||||
|
port: ${MILVUS_PORT:19530}
|
||||||
|
collection: ${MILVUS_COLLECTION:message_summary}
|
||||||
|
username: ${MILVUS_USERNAME:}
|
||||||
|
password: ${MILVUS_PASSWORD:}
|
||||||
|
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
configuration:
|
configuration:
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
|||||||
@ -111,6 +111,11 @@ services:
|
|||||||
- WECOM_ARCHIVE_RSA_KEY=${WECOM_ARCHIVE_RSA_KEY:-}
|
- WECOM_ARCHIVE_RSA_KEY=${WECOM_ARCHIVE_RSA_KEY:-}
|
||||||
- WECOM_ARCHIVE_CALLBACK_TOKEN=${WECOM_ARCHIVE_CALLBACK_TOKEN:-}
|
- WECOM_ARCHIVE_CALLBACK_TOKEN=${WECOM_ARCHIVE_CALLBACK_TOKEN:-}
|
||||||
- WECOM_ARCHIVE_SDK_PATH=${WECOM_ARCHIVE_SDK_PATH:-/app/sdk}
|
- WECOM_ARCHIVE_SDK_PATH=${WECOM_ARCHIVE_SDK_PATH:-/app/sdk}
|
||||||
|
- MILVUS_HOST=${MILVUS_HOST:-8.159.132.53}
|
||||||
|
- MILVUS_PORT=${MILVUS_PORT:-19530}
|
||||||
|
- MILVUS_USERNAME=${MILVUS_USERNAME:-root}
|
||||||
|
- MILVUS_PASSWORD=${MILVUS_PASSWORD:-Milvus}
|
||||||
|
- MILVUS_COLLECTION=${MILVUS_COLLECTION:-message_summary}
|
||||||
# 如需真实企微SDK,取消注释下行并将Linux .so文件放入 ./sdk/linux/
|
# 如需真实企微SDK,取消注释下行并将Linux .so文件放入 ./sdk/linux/
|
||||||
# volumes:
|
# volumes:
|
||||||
# - ./sdk/linux:/app/sdk
|
# - ./sdk/linux:/app/sdk
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Table, Input, Card, Avatar, Tag, Button, Modal, Row, Col, Statistic, Space, Spin } from 'antd'
|
import { Table, Input, Card, Avatar, Tag, Button, Modal, Row, Col, Statistic, Space, Spin, DatePicker, Empty, Typography } from 'antd'
|
||||||
import { UserOutlined } from '@ant-design/icons'
|
import { UserOutlined } from '@ant-design/icons'
|
||||||
|
import type { Dayjs } from 'dayjs'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
import request from '../utils/request'
|
import request from '../utils/request'
|
||||||
import ChatTimeline from '../components/ChatTimeline'
|
import ChatTimeline from '../components/ChatTimeline'
|
||||||
|
|
||||||
@ -31,6 +33,20 @@ interface SessionVO {
|
|||||||
sessionType: string
|
sessionType: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DailySummaryVO {
|
||||||
|
id: number
|
||||||
|
msgTime: string
|
||||||
|
msgType: number
|
||||||
|
fromUser: string
|
||||||
|
toUser: string
|
||||||
|
roomId: string
|
||||||
|
messageContext: string
|
||||||
|
summary: string
|
||||||
|
createdAt: number
|
||||||
|
staffId: string
|
||||||
|
staffName: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function CustomerSessionList() {
|
export default function CustomerSessionList() {
|
||||||
const [data, setData] = useState<CustomerVO[]>([])
|
const [data, setData] = useState<CustomerVO[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@ -43,6 +59,11 @@ export default function CustomerSessionList() {
|
|||||||
const [sessionLoading, setSessionLoading] = useState(false)
|
const [sessionLoading, setSessionLoading] = useState(false)
|
||||||
const [chatVisible, setChatVisible] = useState(false)
|
const [chatVisible, setChatVisible] = useState(false)
|
||||||
const [selectedSession, setSelectedSession] = useState<SessionVO | null>(null)
|
const [selectedSession, setSelectedSession] = useState<SessionVO | null>(null)
|
||||||
|
const [summaryVisible, setSummaryVisible] = useState(false)
|
||||||
|
const [summaryCustomer, setSummaryCustomer] = useState<CustomerVO | null>(null)
|
||||||
|
const [summaryDate, setSummaryDate] = useState<Dayjs>(dayjs())
|
||||||
|
const [summaryList, setSummaryList] = useState<DailySummaryVO[]>([])
|
||||||
|
const [summaryLoading, setSummaryLoading] = useState(false)
|
||||||
|
|
||||||
const loadData = async (page = 1, pageSize = 20) => {
|
const loadData = async (page = 1, pageSize = 20) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@ -101,6 +122,35 @@ export default function CustomerSessionList() {
|
|||||||
setChatVisible(true)
|
setChatVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadDailySummary = async (customerId: string, date: Dayjs) => {
|
||||||
|
setSummaryLoading(true)
|
||||||
|
try {
|
||||||
|
const res: any = await request.get('/v1/archive/daily-summary', {
|
||||||
|
params: {
|
||||||
|
customerId,
|
||||||
|
date: date.format('YYYY-MM-DD'),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setSummaryList(res.data || [])
|
||||||
|
} finally {
|
||||||
|
setSummaryLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDailySummary = (record: CustomerVO) => {
|
||||||
|
setSummaryCustomer(record)
|
||||||
|
setSummaryDate(dayjs())
|
||||||
|
setSummaryVisible(true)
|
||||||
|
loadDailySummary(record.customerId, dayjs())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSummaryDateChange = (date: Dayjs | null) => {
|
||||||
|
if (date && summaryCustomer) {
|
||||||
|
setSummaryDate(date)
|
||||||
|
loadDailySummary(summaryCustomer.customerId, date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: '客户',
|
title: '客户',
|
||||||
@ -183,11 +233,16 @@ export default function CustomerSessionList() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 120,
|
width: 180,
|
||||||
render: (_: any, record: CustomerVO) => (
|
render: (_: any, record: CustomerVO) => (
|
||||||
<Button type="link" size="small" onClick={() => openDetail(record)}>
|
<Space>
|
||||||
查看会话
|
<Button type="link" size="small" onClick={() => openDetail(record)}>
|
||||||
</Button>
|
查看会话
|
||||||
|
</Button>
|
||||||
|
<Button type="link" size="small" onClick={() => openDailySummary(record)}>
|
||||||
|
每日总结
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -330,6 +385,58 @@ export default function CustomerSessionList() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* 每日总结弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Avatar src={summaryCustomer?.avatar} size={32} style={{ background: '#52c41a' }}>
|
||||||
|
{summaryCustomer?.name?.charAt(0)}
|
||||||
|
</Avatar>
|
||||||
|
<span>{summaryCustomer?.name || summaryCustomer?.customerId} 的每日总结</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={summaryVisible}
|
||||||
|
onCancel={() => setSummaryVisible(false)}
|
||||||
|
width={800}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Space>
|
||||||
|
<span>选择日期:</span>
|
||||||
|
<DatePicker value={summaryDate} onChange={handleSummaryDateChange} allowClear={false} />
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<Spin spinning={summaryLoading}>
|
||||||
|
{summaryList.length === 0 ? (
|
||||||
|
<Empty description="暂无当日总结" />
|
||||||
|
) : (
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{summaryList.map((item) => (
|
||||||
|
<Card
|
||||||
|
key={item.id}
|
||||||
|
size="small"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
title={item.staffName || item.staffId || '未知老师'}
|
||||||
|
>
|
||||||
|
<Typography.Text style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{item.summary || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
{item.messageContext && (
|
||||||
|
<div style={{ marginTop: 12, padding: 12, background: '#f6f6f6', borderRadius: 4 }}>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12, whiteSpace: 'pre-wrap' }}>
|
||||||
|
<strong>对话上下文:</strong>
|
||||||
|
<br />
|
||||||
|
{item.messageContext}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Spin>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user