254 lines
12 KiB
Java
254 lines
12 KiB
Java
package com.emotion.service.impl;
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
import com.emotion.dto.request.social.SocialInsightGenerateRequest;
|
|
import com.emotion.dto.request.social.SocialInsightUpdateRequest;
|
|
import com.emotion.dto.response.social.SocialProfileInsightResponse;
|
|
import com.emotion.entity.SocialContentItem;
|
|
import com.emotion.entity.SocialProfileInsight;
|
|
import com.emotion.mapper.SocialContentItemMapper;
|
|
import com.emotion.mapper.SocialProfileInsightMapper;
|
|
import com.emotion.service.SocialInsightService;
|
|
import org.springframework.beans.BeanUtils;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.util.StringUtils;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.time.LocalDateTime;
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.util.ArrayList;
|
|
import java.util.Comparator;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Set;
|
|
import java.util.stream.Collectors;
|
|
|
|
@Service
|
|
public class SocialInsightServiceImpl extends ServiceImpl<SocialProfileInsightMapper, SocialProfileInsight>
|
|
implements SocialInsightService {
|
|
|
|
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
private static final Set<String> STATUS_VALUES = Set.of("suggested", "confirmed", "rejected", "deleted");
|
|
|
|
@Autowired
|
|
private SocialContentItemMapper contentItemMapper;
|
|
|
|
@Override
|
|
public List<SocialProfileInsightResponse> generateInsights(String userId, SocialInsightGenerateRequest request) {
|
|
if (!StringUtils.hasText(userId)) {
|
|
return List.of();
|
|
}
|
|
|
|
LambdaQueryWrapper<SocialContentItem> wrapper = new LambdaQueryWrapper<>();
|
|
wrapper.eq(SocialContentItem::getUserId, userId)
|
|
.eq(SocialContentItem::getIsDeleted, 0)
|
|
.eq(SocialContentItem::getApprovedForAi, 1)
|
|
.isNotNull(SocialContentItem::getContent)
|
|
.orderByDesc(SocialContentItem::getCreateTime);
|
|
if (request != null && request.getSourceItemIds() != null && !request.getSourceItemIds().isEmpty()) {
|
|
wrapper.in(SocialContentItem::getId, request.getSourceItemIds());
|
|
}
|
|
|
|
List<SocialContentItem> contentItems = contentItemMapper.selectList(wrapper);
|
|
List<SocialProfileInsight> saved = new ArrayList<>();
|
|
for (SocialContentItem item : contentItems) {
|
|
for (SocialProfileInsight insight : extractInsights(userId, item)) {
|
|
if (existsActiveInsight(userId, item.getId(), insight.getInsightType(), insight.getLabel())) {
|
|
continue;
|
|
}
|
|
this.save(insight);
|
|
saved.add(insight);
|
|
}
|
|
}
|
|
|
|
return saved.stream()
|
|
.sorted(Comparator.comparing(SocialProfileInsight::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())))
|
|
.map(this::convertToResponse)
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
@Override
|
|
public List<SocialProfileInsightResponse> listByUser(String userId, String status) {
|
|
if (!StringUtils.hasText(userId)) {
|
|
return List.of();
|
|
}
|
|
LambdaQueryWrapper<SocialProfileInsight> wrapper = new LambdaQueryWrapper<>();
|
|
wrapper.eq(SocialProfileInsight::getUserId, userId)
|
|
.eq(SocialProfileInsight::getIsDeleted, 0);
|
|
if (StringUtils.hasText(status)) {
|
|
wrapper.eq(SocialProfileInsight::getStatus, status);
|
|
}
|
|
wrapper.orderByDesc(SocialProfileInsight::getUpdateTime)
|
|
.orderByDesc(SocialProfileInsight::getCreateTime);
|
|
return this.list(wrapper).stream()
|
|
.map(this::convertToResponse)
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
@Override
|
|
public SocialProfileInsightResponse updateByUser(String userId, String id, SocialInsightUpdateRequest request) {
|
|
SocialProfileInsight insight = getOwnedInsight(userId, id);
|
|
if (insight == null || request == null) {
|
|
return null;
|
|
}
|
|
|
|
if (StringUtils.hasText(request.getLabel())) {
|
|
insight.setLabel(request.getLabel().trim());
|
|
insight.setUserEdited(1);
|
|
}
|
|
if (request.getSummary() != null) {
|
|
insight.setSummary(request.getSummary().trim());
|
|
insight.setUserEdited(1);
|
|
}
|
|
if (StringUtils.hasText(request.getStatus())) {
|
|
String nextStatus = request.getStatus().trim().toLowerCase(Locale.ROOT);
|
|
if (!STATUS_VALUES.contains(nextStatus)) {
|
|
throw new IllegalArgumentException("不支持的画像状态");
|
|
}
|
|
insight.setStatus(nextStatus);
|
|
if ("confirmed".equals(nextStatus)) {
|
|
insight.setConfirmedAt(LocalDateTime.now());
|
|
}
|
|
if ("deleted".equals(nextStatus)) {
|
|
insight.setIsDeleted(1);
|
|
insight.setDeletedAt(LocalDateTime.now());
|
|
}
|
|
}
|
|
|
|
this.updateById(insight);
|
|
return convertToResponse(insight);
|
|
}
|
|
|
|
@Override
|
|
public boolean deleteByUser(String userId, String id) {
|
|
SocialProfileInsight insight = getOwnedInsight(userId, id);
|
|
if (insight == null) {
|
|
return false;
|
|
}
|
|
insight.setStatus("deleted");
|
|
insight.setIsDeleted(1);
|
|
insight.setDeletedAt(LocalDateTime.now());
|
|
return this.updateById(insight);
|
|
}
|
|
|
|
private List<SocialProfileInsight> extractInsights(String userId, SocialContentItem item) {
|
|
String content = item.getContent() == null ? "" : item.getContent().trim();
|
|
if (content.isEmpty()) {
|
|
return List.of();
|
|
}
|
|
|
|
List<SocialProfileInsight> insights = new ArrayList<>();
|
|
addIfMatched(insights, userId, item, "value", "职场成长", "用户内容中多次出现工作、职场或创业相关表达,适合转化为事业成长线。",
|
|
content, BigDecimal.valueOf(0.78), "工作", "职场", "老板", "产品", "创业", "公司", "项目");
|
|
addIfMatched(insights, userId, item, "value", "被认可", "用户对被看见、被肯定或证明自己有较强叙事诉求。",
|
|
content, BigDecimal.valueOf(0.74), "认可", "看见", "夸", "肯定", "证明", "成绩", "价值");
|
|
addIfMatched(insights, userId, item, "interest", "旅行探索", "用户表达了对远方、城市或旅行场景的兴趣,可用于开放式人生分支。",
|
|
content, BigDecimal.valueOf(0.70), "旅行", "旅游", "城市", "远方", "大理", "海边", "出发");
|
|
addIfMatched(insights, userId, item, "interest", "创作表达", "用户内容中有写作、音乐、设计或内容创作倾向,适合作为主角天赋线。",
|
|
content, BigDecimal.valueOf(0.72), "写作", "音乐", "创作", "设计", "内容", "画", "摄影");
|
|
addIfMatched(insights, userId, item, "emotion_pattern", "关系修复", "用户对关系、告别或重新理解自己有明显表达,可用于情感修复线。",
|
|
content, BigDecimal.valueOf(0.69), "关系", "分手", "告别", "喜欢", "爱", "朋友", "家人");
|
|
addIfMatched(insights, userId, item, "script_theme", "低谷逆袭", "用户提到压力、失败或低谷,可转化为爽文开端和反转动力。",
|
|
content, BigDecimal.valueOf(0.73), "低谷", "失败", "焦虑", "难过", "压力", "崩溃", "迷茫");
|
|
|
|
if (insights.isEmpty()) {
|
|
insights.add(buildInsight(userId, item, "script_theme", "自我成长",
|
|
"这段内容适合作为自我探索和人生选择的素材。",
|
|
excerpt(content), BigDecimal.valueOf(0.58)));
|
|
}
|
|
return insights;
|
|
}
|
|
|
|
private void addIfMatched(List<SocialProfileInsight> insights, String userId, SocialContentItem item,
|
|
String type, String label, String summary, String content,
|
|
BigDecimal confidence, String... keywords) {
|
|
String lower = content.toLowerCase(Locale.ROOT);
|
|
for (String keyword : keywords) {
|
|
if (lower.contains(keyword.toLowerCase(Locale.ROOT))) {
|
|
insights.add(buildInsight(userId, item, type, label, summary, excerptAround(content, keyword), confidence));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private SocialProfileInsight buildInsight(String userId, SocialContentItem item, String type, String label,
|
|
String summary, String evidence, BigDecimal confidence) {
|
|
SocialProfileInsight insight = new SocialProfileInsight();
|
|
insight.setUserId(userId);
|
|
insight.setSourceItemId(item.getId());
|
|
insight.setInsightType(type);
|
|
insight.setLabel(label);
|
|
insight.setSummary(summary);
|
|
insight.setEvidenceExcerpt(evidence);
|
|
insight.setConfidence(confidence);
|
|
insight.setStatus("suggested");
|
|
insight.setUserEdited(0);
|
|
insight.setIsDeleted(0);
|
|
return insight;
|
|
}
|
|
|
|
private boolean existsActiveInsight(String userId, String sourceItemId, String type, String label) {
|
|
LambdaQueryWrapper<SocialProfileInsight> wrapper = new LambdaQueryWrapper<>();
|
|
wrapper.eq(SocialProfileInsight::getUserId, userId)
|
|
.eq(SocialProfileInsight::getSourceItemId, sourceItemId)
|
|
.eq(SocialProfileInsight::getInsightType, type)
|
|
.eq(SocialProfileInsight::getLabel, label)
|
|
.eq(SocialProfileInsight::getIsDeleted, 0);
|
|
return this.count(wrapper) > 0;
|
|
}
|
|
|
|
private SocialProfileInsight getOwnedInsight(String userId, String id) {
|
|
if (!StringUtils.hasText(userId) || !StringUtils.hasText(id)) {
|
|
return null;
|
|
}
|
|
LambdaQueryWrapper<SocialProfileInsight> wrapper = new LambdaQueryWrapper<>();
|
|
wrapper.eq(SocialProfileInsight::getUserId, userId)
|
|
.eq(SocialProfileInsight::getId, id)
|
|
.eq(SocialProfileInsight::getIsDeleted, 0);
|
|
return this.getOne(wrapper, false);
|
|
}
|
|
|
|
private String excerptAround(String content, String keyword) {
|
|
int index = content.indexOf(keyword);
|
|
if (index < 0) {
|
|
return excerpt(content);
|
|
}
|
|
int start = Math.max(0, index - 45);
|
|
int end = Math.min(content.length(), index + keyword.length() + 65);
|
|
return excerpt(content.substring(start, end));
|
|
}
|
|
|
|
private String excerpt(String text) {
|
|
String normalized = text.replaceAll("\\s+", " ").trim();
|
|
if (normalized.length() <= 120) {
|
|
return normalized;
|
|
}
|
|
return normalized.substring(0, 120) + "...";
|
|
}
|
|
|
|
private SocialProfileInsightResponse convertToResponse(SocialProfileInsight insight) {
|
|
SocialProfileInsightResponse response = new SocialProfileInsightResponse();
|
|
BeanUtils.copyProperties(insight, response);
|
|
response.setId(insight.getId());
|
|
response.setUserEdited(insight.getUserEdited() != null && insight.getUserEdited() == 1);
|
|
response.setSourceDeleted(isSourceDeleted(insight.getSourceItemId()));
|
|
if (insight.getCreateTime() != null) {
|
|
response.setCreateTime(insight.getCreateTime().format(DATE_TIME_FORMATTER));
|
|
}
|
|
if (insight.getUpdateTime() != null) {
|
|
response.setUpdateTime(insight.getUpdateTime().format(DATE_TIME_FORMATTER));
|
|
}
|
|
return response;
|
|
}
|
|
|
|
private Boolean isSourceDeleted(String sourceItemId) {
|
|
if (!StringUtils.hasText(sourceItemId)) {
|
|
return true;
|
|
}
|
|
SocialContentItem item = contentItemMapper.selectById(sourceItemId);
|
|
return item == null || (item.getIsDeleted() != null && item.getIsDeleted() == 1);
|
|
}
|
|
}
|