Files
happy-life-star/server/src/main/java/com/emotion/service/impl/SocialInsightServiceImpl.java
T

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);
}
}