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 implements SocialInsightService { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private static final Set STATUS_VALUES = Set.of("suggested", "confirmed", "rejected", "deleted"); @Autowired private SocialContentItemMapper contentItemMapper; @Override public List generateInsights(String userId, SocialInsightGenerateRequest request) { if (!StringUtils.hasText(userId)) { return List.of(); } LambdaQueryWrapper 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 contentItems = contentItemMapper.selectList(wrapper); List 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 listByUser(String userId, String status) { if (!StringUtils.hasText(userId)) { return List.of(); } LambdaQueryWrapper 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 extractInsights(String userId, SocialContentItem item) { String content = item.getContent() == null ? "" : item.getContent().trim(); if (content.isEmpty()) { return List.of(); } List 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 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 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 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); } }