|
|
@@ -32,6 +32,7 @@ import com.fs.live.domain.LiveWatchLog;
|
|
|
import com.fs.live.mapper.LiveWatchLogMapper;
|
|
|
import com.fs.qw.domain.*;
|
|
|
import com.fs.qw.mapper.QwExternalContactMapper;
|
|
|
+import com.fs.qw.mapper.QwGroupChatUserMapper;
|
|
|
import com.fs.qw.mapper.QwUserMapper;
|
|
|
import com.fs.qw.service.IQwCompanyService;
|
|
|
import com.fs.qw.service.IQwGroupChatService;
|
|
|
@@ -121,6 +122,14 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
/** his.config 任务级缓存,定时刷新,避免课程链路反复查库解析 */
|
|
|
private volatile FSSysConfig cachedHisConfig;
|
|
|
|
|
|
+ /** 全量查询内存缓存(公司小程序/部门/公司用户/公司列表),55分钟失效,避免每小时任务重试时重复全表扫描 */
|
|
|
+ private volatile long fullQueryCacheTime = 0;
|
|
|
+ private volatile Map<Long, Map<Integer, List<CompanyMiniapp>>> cachedMiniMap;
|
|
|
+ private volatile Map<Long, CompanyDept> cachedDeptMiniAppMap;
|
|
|
+ private volatile Map<Long, Long> cachedCompanyUserDeptMap;
|
|
|
+ private volatile List<Company> cachedCompanies;
|
|
|
+ private static final long FULL_QUERY_CACHE_TTL_MS = 55 * 60 * 1000L;
|
|
|
+
|
|
|
/** 用户日志并行度(8C 机器且库连接有限,保守控制) */
|
|
|
private static final int USER_LOG_PARALLELISM = 4;
|
|
|
|
|
|
@@ -131,9 +140,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
private static final int WRITE_QUEUE_CAPACITY = 50000;
|
|
|
|
|
|
/** 主写入消费者线程数(IO 等待型,略增吞吐,不把连接池打满) */
|
|
|
- private static final int QW_SOP_LOGS_CONSUMERS = 3;
|
|
|
- private static final int WATCH_LOG_CONSUMERS = 2;
|
|
|
- private static final int COURSE_LINK_CONSUMERS = 2;
|
|
|
+ private static final int QW_SOP_LOGS_CONSUMERS = 5;
|
|
|
+ private static final int WATCH_LOG_CONSUMERS = 3;
|
|
|
+ private static final int COURSE_LINK_CONSUMERS = 3;
|
|
|
|
|
|
|
|
|
/** 单次刷库目标条数;队列有积压时应尽量凑满再写 */
|
|
|
@@ -268,6 +277,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
@Autowired
|
|
|
private QwExternalContactMapper qwExternalContactMapper;
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private QwGroupChatUserMapper qwGroupChatUserMapper;
|
|
|
+
|
|
|
/** 用户日志有限并行(避免同类 @Async 自调用失效) */
|
|
|
@Autowired
|
|
|
@org.springframework.beans.factory.annotation.Qualifier("sopTaskExecutor")
|
|
|
@@ -447,8 +459,16 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
String[] array = sopUserLogsVos.stream().map(SopUserLogsVo::getChatId).filter(StringUtils::isNotEmpty).toArray(String[]::new);
|
|
|
Map<String, QwGroupChat> groupChatMap = new HashMap<>();
|
|
|
if (array.length > 0) {
|
|
|
- List<QwGroupChat> qwGroupChatList = qwGroupChatService.selectQwGroupChatByChatIds(array);
|
|
|
- List<QwGroupChatUser> qwGroupChatUserList = qwGroupChatUserService.selectQwGroupChatUserByChatIds(array);
|
|
|
+ // 群聊和群聊用户分批查询,避免单次 IN 过大拖垮优化器
|
|
|
+ int chatBatchSize = 500;
|
|
|
+ List<QwGroupChat> qwGroupChatList = new ArrayList<>();
|
|
|
+ List<QwGroupChatUser> qwGroupChatUserList = new ArrayList<>();
|
|
|
+ for (int i = 0; i < array.length; i += chatBatchSize) {
|
|
|
+ String[] batch = Arrays.copyOfRange(array, i, Math.min(i + chatBatchSize, array.length));
|
|
|
+ qwGroupChatList.addAll(qwGroupChatService.selectQwGroupChatByChatIds(batch));
|
|
|
+ // 只查 chat_id 和 user_id,走 unique_chat_user 覆盖索引,避免 select * 回表
|
|
|
+ qwGroupChatUserList.addAll(qwGroupChatUserMapper.selectChatIdAndUserIdByChatIds(batch));
|
|
|
+ }
|
|
|
List<String> groupChatUserIds = PubFun.listToNewList(qwGroupChatUserList, QwGroupChatUser::getUserId);
|
|
|
if(!groupChatUserIds.isEmpty()){
|
|
|
List<GroupUserExternalVo> userList = qwExternalContactMapper.selectByGroupUser(groupChatUserIds);
|
|
|
@@ -465,14 +485,29 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
Map<String, List<SopUserLogsVo>> sopLogsGroupedById = sopUserLogsVos.stream()
|
|
|
.collect(Collectors.groupingBy(SopUserLogsVo::getSopId));
|
|
|
|
|
|
- // 查询公司关联小程序数据
|
|
|
- List<CompanyMiniapp> miniList = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().orderByAsc("sort_num"));
|
|
|
-
|
|
|
- Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap = miniList.stream().collect(Collectors.groupingBy(CompanyMiniapp::getCompanyId, Collectors.groupingBy(CompanyMiniapp::getType)));
|
|
|
- Map<Long, CompanyDept> deptMiniAppMap = MiniAppIdResolver.buildDeptMiniAppMap(companyDeptMapper.queryDeptDataAll());
|
|
|
- Map<Long, Long> companyUserDeptMap = MiniAppIdResolver.buildCompanyUserDeptMap(companyUserMapper.selectAllCompanyUserList());
|
|
|
-
|
|
|
- List<Company> companies = companyMapper.selectCompanyAllList();
|
|
|
+ // D 优化:全量查询使用内存缓存,55分钟失效(留5分钟余量,避免跨小时数据不一致)
|
|
|
+ long fullQueryStart = System.currentTimeMillis();
|
|
|
+ long nowMs = System.currentTimeMillis();
|
|
|
+ if (cachedMiniMap == null || nowMs - fullQueryCacheTime > FULL_QUERY_CACHE_TTL_MS) {
|
|
|
+ List<CompanyMiniapp> miniList = companyMiniappService.list(new QueryWrapper<CompanyMiniapp>().orderByAsc("sort_num"));
|
|
|
+ cachedMiniMap = miniList.stream().collect(Collectors.groupingBy(CompanyMiniapp::getCompanyId, Collectors.groupingBy(CompanyMiniapp::getType)));
|
|
|
+ cachedDeptMiniAppMap = MiniAppIdResolver.buildDeptMiniAppMap(companyDeptMapper.queryDeptDataAll());
|
|
|
+ cachedCompanyUserDeptMap = MiniAppIdResolver.buildCompanyUserDeptMap(companyUserMapper.selectAllCompanyUserList());
|
|
|
+ cachedCompanies = companyMapper.selectCompanyAllList();
|
|
|
+ fullQueryCacheTime = nowMs;
|
|
|
+ log.info("全量查询缓存已刷新,耗时 {} 毫秒", System.currentTimeMillis() - fullQueryStart);
|
|
|
+ }
|
|
|
+ Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap = cachedMiniMap;
|
|
|
+ Map<Long, CompanyDept> deptMiniAppMap = cachedDeptMiniAppMap;
|
|
|
+ Map<Long, Long> companyUserDeptMap = cachedCompanyUserDeptMap;
|
|
|
+ List<Company> companies = cachedCompanies;
|
|
|
+
|
|
|
+ // A+B 优化:预加载 QwUser 和 CompanyUser,避免每个 userLog 重复查 Redis
|
|
|
+ long preloadStart = System.currentTimeMillis();
|
|
|
+ Map<String, QwUser> qwUserMap = preloadQwUsers(sopUserLogsVos);
|
|
|
+ Map<Long, CompanyUser> companyUserMap = preloadCompanyUsers(qwUserMap);
|
|
|
+ log.info("预加载 QwUser {} 条、CompanyUser {} 条,耗时 {} 毫秒",
|
|
|
+ qwUserMap.size(), companyUserMap.size(), System.currentTimeMillis() - preloadStart);
|
|
|
|
|
|
log.info("共分组 {} 个 SOP ID 进行处理。", sopLogsGroupedById.size());
|
|
|
|
|
|
@@ -482,7 +517,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
List<SopUserLogsVo> userLogsVos = entry.getValue();
|
|
|
try {
|
|
|
processSopGroup(sopId, userLogsVos, currentTime, groupChatMap, config, miniMap, companies,
|
|
|
- deptMiniAppMap, companyUserDeptMap);
|
|
|
+ deptMiniAppMap, companyUserDeptMap, qwUserMap, companyUserMap);
|
|
|
} catch (Exception e) {
|
|
|
log.error("处理 SOP ID {} 时发生异常: {}", sopId, e.getMessage(), e);
|
|
|
}
|
|
|
@@ -504,9 +539,11 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
)
|
|
|
public void processSopGroupAsync(String sopId, List<SopUserLogsVo> userLogsVos, CountDownLatch latch ,LocalDateTime currentTime,
|
|
|
Map<String, QwGroupChat> groupChatMap,CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
|
|
|
- List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap) {
|
|
|
+ List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
|
|
|
+ Map<String, QwUser> qwUserMap, Map<Long, CompanyUser> companyUserMap) {
|
|
|
try {
|
|
|
- processSopGroup(sopId, userLogsVos,currentTime, groupChatMap, config,miniMap,companies, deptMiniAppMap, companyUserDeptMap);
|
|
|
+ processSopGroup(sopId, userLogsVos,currentTime, groupChatMap, config,miniMap,companies, deptMiniAppMap, companyUserDeptMap,
|
|
|
+ qwUserMap, companyUserMap);
|
|
|
} catch (Exception e) {
|
|
|
log.error("处理 SOP ID {} 时发生异常: {}", sopId, e.getMessage(), e);
|
|
|
} finally {
|
|
|
@@ -517,7 +554,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
|
|
|
private void processSopGroup(String sopId, List<SopUserLogsVo> userLogsVos,LocalDateTime currentTime, Map<String,
|
|
|
QwGroupChat> groupChatMap,CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
|
|
|
- List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap) throws Exception {
|
|
|
+ List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
|
|
|
+ Map<String, QwUser> qwUserMap, Map<Long, CompanyUser> companyUserMap) throws Exception {
|
|
|
QwSopRuleTimeVO ruleTimeVO = sopMapper.selectQwSopByClickHouseId(sopId);
|
|
|
|
|
|
if (ruleTimeVO == null) {
|
|
|
@@ -570,6 +608,21 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
infoMapByUserLogsId.size(),
|
|
|
System.currentTimeMillis() - preloadStart);
|
|
|
|
|
|
+ // C 优化:SOP 级预加载外部联系人,避免每个 userLog 的 insertSopUserLogs 重复分批 IN 查
|
|
|
+ Map<Long, QwExternalContact> sopContactMap = Collections.emptyMap();
|
|
|
+ if (CollectionUtils.isNotEmpty(allInfos)) {
|
|
|
+ long contactPreloadStart = System.currentTimeMillis();
|
|
|
+ List<Long> extIds = allInfos.stream()
|
|
|
+ .map(SopUserLogsInfo::getExternalId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .distinct()
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ sopContactMap = loadExternalContactsByIds(extIds);
|
|
|
+ log.info("SOP ID {} 预加载外部联系人 {} 条,耗时 {} 毫秒",
|
|
|
+ sopId, sopContactMap.size(), System.currentTimeMillis() - contactPreloadStart);
|
|
|
+ }
|
|
|
+ final Map<Long, QwExternalContact> finalSopContactMap = sopContactMap;
|
|
|
+
|
|
|
CountDownLatch userLogsLatch = new CountDownLatch(userLogsVos.size());
|
|
|
Semaphore userLogSemaphore = new Semaphore(USER_LOG_PARALLELISM);
|
|
|
FeishuDirectDocShardAllocator feishuShardAllocator = FeishuDirectDocShardAllocator.forAutoSop(config);
|
|
|
@@ -586,7 +639,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
try {
|
|
|
processUserLog(logVo, ruleTimeVO, rulesList, currentTime, groupChatMap, qwCompany.getMiniAppId(),
|
|
|
config, miniMap, companies, deptMiniAppMap, companyUserDeptMap, feishuShardAllocator,
|
|
|
- infoMapByUserLogsId);
|
|
|
+ infoMapByUserLogsId, qwUserMap, companyUserMap, finalSopContactMap);
|
|
|
} catch (Exception e) {
|
|
|
log.error("处理用户日志 {} 时发生异常: {}", logVo.getId(), e.getMessage(), e);
|
|
|
} finally {
|
|
|
@@ -617,10 +670,13 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
String miniAppId,CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
|
|
|
List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
|
|
|
FeishuDirectDocShardAllocator feishuShardAllocator,
|
|
|
- Map<String, List<SopUserLogsInfo>> infoMapByUserLogsId) {
|
|
|
+ Map<String, List<SopUserLogsInfo>> infoMapByUserLogsId,
|
|
|
+ Map<String, QwUser> qwUserMap, Map<Long, CompanyUser> companyUserMap,
|
|
|
+ Map<Long, QwExternalContact> sopContactMap) {
|
|
|
try {
|
|
|
processUserLog(logVo, ruleTimeVO, tempSettings,currentTime, groupChatMap, miniAppId, config,miniMap,companies,
|
|
|
- deptMiniAppMap, companyUserDeptMap, feishuShardAllocator, infoMapByUserLogsId);
|
|
|
+ deptMiniAppMap, companyUserDeptMap, feishuShardAllocator, infoMapByUserLogsId,
|
|
|
+ qwUserMap, companyUserMap, sopContactMap);
|
|
|
} catch (Exception e) {
|
|
|
log.error("处理用户日志 {} 时发生异常: {}", logVo.getId(), e.getMessage(), e);
|
|
|
} finally {
|
|
|
@@ -634,7 +690,9 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
CourseConfig config,Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap,
|
|
|
List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
|
|
|
FeishuDirectDocShardAllocator feishuShardAllocator,
|
|
|
- Map<String, List<SopUserLogsInfo>> infoMapByUserLogsId) {
|
|
|
+ Map<String, List<SopUserLogsInfo>> infoMapByUserLogsId,
|
|
|
+ Map<String, QwUser> qwUserMap, Map<Long, CompanyUser> companyUserMap,
|
|
|
+ Map<Long, QwExternalContact> sopContactMap) {
|
|
|
try {
|
|
|
|
|
|
LocalDate startDate = LocalDate.parse(logVo.getStartTime(), DATE_FORMATTER);
|
|
|
@@ -681,7 +739,12 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
|
|
|
|
|
|
//获取企业微信员工的称呼//从redis里或者从库里取
|
|
|
- QwUser qwUserByRedis = qwExternalContactService.getQwUserByRedis(logVo.getCorpId(),logVo.getQwUserId());
|
|
|
+ // A 优化:优先从预加载 Map 获取 QwUser,未命中再回退查 Redis(保持原逻辑)
|
|
|
+ String qwUserKey = logVo.getCorpId() + ":" + logVo.getQwUserId();
|
|
|
+ QwUser qwUserByRedis = qwUserMap != null ? qwUserMap.get(qwUserKey) : null;
|
|
|
+ if (qwUserByRedis == null) {
|
|
|
+ qwUserByRedis = qwExternalContactService.getQwUserByRedis(logVo.getCorpId(), logVo.getQwUserId());
|
|
|
+ }
|
|
|
if (qwUserByRedis==null){
|
|
|
log.error("无企微员工信息 {} 跳过处理。:{}", logVo.getUserId(),logVo.getCorpId());
|
|
|
return;
|
|
|
@@ -697,7 +760,11 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- CompanyUser companyUser = companyUserService.selectCompanyUserByIdForRedis(Long.valueOf(companyUserId));
|
|
|
+ // B 优化:优先从预加载 Map 获取 CompanyUser,未命中再回退查 Redis(保持原逻辑)
|
|
|
+ CompanyUser companyUser = companyUserMap != null ? companyUserMap.get(Long.valueOf(companyUserId)) : null;
|
|
|
+ if (companyUser == null) {
|
|
|
+ companyUser = companyUserService.selectCompanyUserByIdForRedis(Long.valueOf(companyUserId));
|
|
|
+ }
|
|
|
if (Objects.nonNull(companyUser)) {
|
|
|
if (!StringUtil.strIsNullOrEmpty(companyUser.getDomain())) {
|
|
|
logVo.setDomain(companyUser.getDomain().trim());
|
|
|
@@ -899,7 +966,7 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
insertSopUserLogs(sopUserLogsInfos, logVo, sendTime, ruleTimeVO, content, qwUserId,
|
|
|
companyUserId, companyId, qwUserByRedis.getWelcomeText(),qwUserByRedis.getQwUserName(),
|
|
|
groupChatMap, miniAppId,config,miniMap, sendMsgType,companies, deptMiniAppMap, companyUserDeptMap,
|
|
|
- feishuShardAllocator);
|
|
|
+ feishuShardAllocator, sopContactMap);
|
|
|
|
|
|
}
|
|
|
} catch (Exception e) {
|
|
|
@@ -913,6 +980,61 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
}
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
+ * A 优化:预加载 QwUser,按 corpId:qwUserId 去重后逐个查 Redis/DB,构建 Map。
|
|
|
+ * 同一销售对应多个 userLog 时只查一次,避免并行线程内重复查 Redis。
|
|
|
+ */
|
|
|
+ private Map<String, QwUser> preloadQwUsers(List<SopUserLogsVo> sopUserLogsVos) {
|
|
|
+ if (CollectionUtils.isEmpty(sopUserLogsVos)) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Set<String> keys = sopUserLogsVos.stream()
|
|
|
+ .filter(e -> StringUtils.isNotEmpty(e.getCorpId()) && StringUtils.isNotEmpty(e.getQwUserId()))
|
|
|
+ .map(e -> e.getCorpId() + ":" + e.getQwUserId())
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ Map<String, QwUser> map = new HashMap<>(keys.size());
|
|
|
+ for (String key : keys) {
|
|
|
+ int idx = key.indexOf(':');
|
|
|
+ String corpId = key.substring(0, idx);
|
|
|
+ String userId = key.substring(idx + 1);
|
|
|
+ try {
|
|
|
+ QwUser qwUser = qwExternalContactService.getQwUserByRedis(corpId, userId);
|
|
|
+ if (qwUser != null) {
|
|
|
+ map.put(key, qwUser);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("预加载 QwUser 失败, corpId={}, userId={}", corpId, userId, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * B 优化:从已加载的 QwUser 中收集 companyUserId,去重后逐个查 Redis/DB,构建 Map。
|
|
|
+ */
|
|
|
+ private Map<Long, CompanyUser> preloadCompanyUsers(Map<String, QwUser> qwUserMap) {
|
|
|
+ if (qwUserMap == null || qwUserMap.isEmpty()) {
|
|
|
+ return Collections.emptyMap();
|
|
|
+ }
|
|
|
+ Set<Long> companyUserIds = qwUserMap.values().stream()
|
|
|
+ .map(QwUser::getCompanyUserId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .filter(id -> id > 0)
|
|
|
+ .collect(Collectors.toSet());
|
|
|
+ Map<Long, CompanyUser> map = new HashMap<>(companyUserIds.size());
|
|
|
+ for (Long id : companyUserIds) {
|
|
|
+ try {
|
|
|
+ CompanyUser companyUser = companyUserService.selectCompanyUserByIdForRedis(id);
|
|
|
+ if (companyUser != null) {
|
|
|
+ map.put(id, companyUser);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("预加载 CompanyUser 失败, companyUserId={}", id, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 按主键分批加载企微客户。单次 IN 不超过 CONTACT_IN_CHUNK_SIZE,避免大 IN 拖垮优化器。
|
|
|
*/
|
|
|
@@ -1002,7 +1124,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
Map<String, QwGroupChat> groupChatMap,String miniAppId,CourseConfig config,
|
|
|
Map<Long, Map<Integer, List<CompanyMiniapp>>> miniMap, Integer sendMsgType,
|
|
|
List<Company> companies, Map<Long, CompanyDept> deptMiniAppMap, Map<Long, Long> companyUserDeptMap,
|
|
|
- FeishuDirectDocShardAllocator feishuShardAllocator) {
|
|
|
+ FeishuDirectDocShardAllocator feishuShardAllocator,
|
|
|
+ Map<Long, QwExternalContact> sopContactMap) {
|
|
|
String formattedSendTime = sendTime.toInstant()
|
|
|
.atZone(ZoneId.systemDefault())
|
|
|
.format(DATE_TIME_FORMATTER);
|
|
|
@@ -1108,14 +1231,20 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
List<SopUserLogsInfo> skippedCustomers = new ArrayList<>();
|
|
|
|
|
|
// 课程消息需要客户称呼等字段:按 externalId 分批 IN 预加载,避免逐条 selectById
|
|
|
- Map<Long, QwExternalContact> contactMap = Collections.emptyMap();
|
|
|
- if (type == 2 && CollectionUtils.isNotEmpty(sopUserLogsInfos)) {
|
|
|
- List<Long> extIds = sopUserLogsInfos.stream()
|
|
|
- .map(SopUserLogsInfo::getExternalId)
|
|
|
- .filter(Objects::nonNull)
|
|
|
- .distinct()
|
|
|
- .collect(Collectors.toList());
|
|
|
- contactMap = loadExternalContactsByIds(extIds);
|
|
|
+ // C 优化:优先使用 SOP 级预加载的外部联系人 Map,未命中再回退分批 IN 查(保持原逻辑)
|
|
|
+ Map<Long, QwExternalContact> contactMap;
|
|
|
+ if (sopContactMap != null && !sopContactMap.isEmpty()) {
|
|
|
+ contactMap = sopContactMap;
|
|
|
+ } else {
|
|
|
+ contactMap = Collections.emptyMap();
|
|
|
+ if (type == 2 && CollectionUtils.isNotEmpty(sopUserLogsInfos)) {
|
|
|
+ List<Long> extIds = sopUserLogsInfos.stream()
|
|
|
+ .map(SopUserLogsInfo::getExternalId)
|
|
|
+ .filter(Objects::nonNull)
|
|
|
+ .distinct()
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ contactMap = loadExternalContactsByIds(extIds);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
Map<Long, QwExternalContact> finalContactMap = contactMap;
|
|
|
@@ -1838,16 +1967,16 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
|
|
|
break;
|
|
|
case "17":
|
|
|
- // 飞书看课链接(旧):固定免授权发送,不再按模板授权开关分支
|
|
|
+ // 飞书看课(固定免授权):一批客户复用同一飞书文档链接;不再走授权/领取页(领取页是 contentType=18)
|
|
|
setting.setFeishuNeedAuth(0);
|
|
|
- // 飞书看课链接:失败仅记录日志并跳过本条,不阻塞整批 SOP 生成
|
|
|
+ // 失败仅记录日志并跳过本条,不阻塞整批 SOP 生成
|
|
|
try {
|
|
|
Long companyIdLong = Long.parseLong(companyId);
|
|
|
Long companyUserIdLong = Long.parseLong(companyUserId);
|
|
|
Long externalIdLong = null;
|
|
|
String chatIdForFeishu = null;
|
|
|
if (isGroupChat) {
|
|
|
- // 群聊飞书发课:按群成员写看课记录(对齐小程序 case 4);发送仍只有一条群消息
|
|
|
+ // 群聊飞书发课:按群成员写看课记录;群消息仍发一条共用飞书链接
|
|
|
try {
|
|
|
groupChat.getChatUserList().stream().filter(e -> e.getUserList() != null && !e.getUserList().isEmpty()).forEach(e -> {
|
|
|
Map<String, GroupUserExternalVo> userMap = PubFun.listToMapByGroupObject(e.getUserList(), GroupUserExternalVo::getUserId);
|
|
|
@@ -1872,46 +2001,32 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
Map<String, String> feiShuLinkMap = createFeiShuLinkByMiniApp(setting, sopLogs.getCorpId(), sendTime, courseId, videoId,
|
|
|
qwUserId, companyUserId, companyId, externalIdLong, chatIdForFeishu, cachedCourseConfig);
|
|
|
String shortLink = feiShuLinkMap.get("link");
|
|
|
-
|
|
|
- String feishuLink;
|
|
|
- if (isGroupChat) {
|
|
|
- // 群:发课时不建飞书文档,只发领取页;领取时按当前用户生成独立飞书链接
|
|
|
- feishuLink = buildFeishuClaimPageUrl(shortLink, cachedCourseConfig);
|
|
|
- if (StringUtils.isEmpty(feishuLink)) {
|
|
|
- log.error("生成飞书群领取链接失败,跳过本条: sopId={}, videoId={}, chatId={}",
|
|
|
- logVo.getSopId(), videoId, chatIdForFeishu);
|
|
|
- recordFeishuLinkGenerationFailed(logVo, sendTime, content.getTime(), qwUserId, qwUserName,
|
|
|
- companyUserId, companyId, logVo.getCorpId(), sopLogs.getExternalId(), fsUserId, videoId,
|
|
|
- "生成飞书群领取链接失败");
|
|
|
- return;
|
|
|
- }
|
|
|
- } else {
|
|
|
- Integer directDocShard = null;
|
|
|
- if (feishuShardAllocator != null) {
|
|
|
- directDocShard = feishuShardAllocator.nextShard(companyUserIdLong, videoId, courseId,
|
|
|
- setting.getFeishuAccountId());
|
|
|
- }
|
|
|
- feishuLink = feiShuService.resolveFeishuSendLink(videoId, companyIdLong, courseId, companyUserIdLong,
|
|
|
- shortLink, setting.getFeishuAccountId(), 0, directDocShard, fsUserId);
|
|
|
- if (StringUtils.isEmpty(feishuLink)) {
|
|
|
- log.error("生成飞书注册链接失败,跳过本条: sopId={}, videoId={}, externalId={}",
|
|
|
- logVo.getSopId(), videoId, externalId);
|
|
|
- recordFeishuLinkGenerationFailed(logVo, sendTime, content.getTime(), qwUserId, qwUserName,
|
|
|
- companyUserId, companyId, logVo.getCorpId(), sopLogs.getExternalId(), fsUserId, videoId,
|
|
|
- "生成飞书注册链接失败");
|
|
|
- return;
|
|
|
- }
|
|
|
+ Integer directDocShard = null;
|
|
|
+ if (feishuShardAllocator != null) {
|
|
|
+ directDocShard = feishuShardAllocator.nextShard(companyUserIdLong, videoId, courseId,
|
|
|
+ setting.getFeishuAccountId());
|
|
|
+ }
|
|
|
+ // 免授权直出飞书文档;同销售+课节+账号+天+分片复用,不传 fsUserId
|
|
|
+ String feishuLink = feiShuService.resolveFeishuSendLink(videoId, companyIdLong, courseId, companyUserIdLong,
|
|
|
+ shortLink, setting.getFeishuAccountId(), 0, directDocShard, null);
|
|
|
+ if (StringUtils.isEmpty(feishuLink)) {
|
|
|
+ log.error("生成飞书免授权链接失败,跳过本条: sopId={}, videoId={}, externalId={}, chatId={}",
|
|
|
+ logVo.getSopId(), videoId, externalId, chatIdForFeishu);
|
|
|
+ recordFeishuLinkGenerationFailed(logVo, sendTime, content.getTime(), qwUserId, qwUserName,
|
|
|
+ companyUserId, companyId, logVo.getCorpId(), sopLogs.getExternalId(), fsUserId, videoId,
|
|
|
+ "生成飞书免授权链接失败");
|
|
|
+ return;
|
|
|
}
|
|
|
setting.setLinkUrl(feishuLink);
|
|
|
setting.setIsBindUrl("2");
|
|
|
// 链接卡片标题/描述/封面为空时兜底,避免 PAD 发出空卡片
|
|
|
fillFeishuLinkCardDefaults(setting, videoId, courseId);
|
|
|
} catch (Exception e) {
|
|
|
- log.error("生成飞书注册链接异常,跳过本条: sopId={}, videoId={}, externalId={}",
|
|
|
+ log.error("生成飞书免授权链接异常,跳过本条: sopId={}, videoId={}, externalId={}",
|
|
|
logVo.getSopId(), videoId, externalId, e);
|
|
|
recordFeishuLinkGenerationFailed(logVo, sendTime, content.getTime(), qwUserId, qwUserName,
|
|
|
companyUserId, companyId, logVo.getCorpId(), sopLogs.getExternalId(), fsUserId, videoId,
|
|
|
- "生成飞书注册链接失败: " + e.getMessage());
|
|
|
+ "生成飞书免授权链接失败: " + e.getMessage());
|
|
|
return;
|
|
|
}
|
|
|
break;
|
|
|
@@ -2339,8 +2454,8 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 群飞书(17):发课时只拼领取页,不调飞书建文档;领取时再按当前用户生成独立飞书链接。
|
|
|
- * 格式与飞书NEW一致:{realLinkDomainName}/?uuid={shortLink}&appid={feishuMpAppId}
|
|
|
+ * 拼飞书领取页(仅 contentType=18 使用)。
|
|
|
+ * 格式:{realLinkDomainName}/?uuid={shortLink}&appid={feishuMpAppId}
|
|
|
*/
|
|
|
private String buildFeishuClaimPageUrl(String shortLink, CourseConfig config) {
|
|
|
if (StringUtils.isEmpty(shortLink) || config == null
|
|
|
@@ -2610,7 +2725,6 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
boolean offered = qwSopLogsQueue.offer(sopLogs, 5, TimeUnit.SECONDS);
|
|
|
if (!offered) {
|
|
|
log.error("QwSopLogs 队列已满,无法添加日志: {}", JSON.toJSONString(sopLogs));
|
|
|
- // 处理队列已满的情况,例如记录到失败队列或持久化存储
|
|
|
}
|
|
|
} catch (InterruptedException e) {
|
|
|
Thread.currentThread().interrupt();
|
|
|
@@ -2626,7 +2740,6 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
|
|
|
boolean offered = watchLogsQueue.offer(watchLog, 5, TimeUnit.SECONDS);
|
|
|
if (!offered) {
|
|
|
log.error("FsCourseWatchLog 队列已满,无法添加日志: {}", JSON.toJSONString(watchLog));
|
|
|
- // 处理队列已满的情况,例如记录到失败队列或持久化存储
|
|
|
}
|
|
|
} catch (InterruptedException e) {
|
|
|
Thread.currentThread().interrupt();
|