package com.emotion.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.emotion.dto.request.social.SocialContentLinkImportRequest; import com.emotion.dto.request.social.SocialContentManualImportRequest; import com.emotion.dto.response.social.SocialContentItemResponse; import com.emotion.entity.SocialContentItem; import com.emotion.entity.SocialProfileInsight; import com.emotion.entity.UserConsentLog; import com.emotion.mapper.SocialContentItemMapper; import com.emotion.mapper.SocialProfileInsightMapper; import com.emotion.mapper.UserConsentLogMapper; import com.emotion.service.SocialContentService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.stream.Collectors; @Service public class SocialContentServiceImpl extends ServiceImpl implements SocialContentService { private static final Set PLATFORMS = Set.of("xiaohongshu", "weibo", "wechat", "other"); private static final int MAX_CONTENT_LENGTH = 20000; private static final long MAX_SCREENSHOT_SIZE = 5L * 1024L * 1024L; private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Autowired private UserConsentLogMapper userConsentLogMapper; @Autowired private SocialProfileInsightMapper socialProfileInsightMapper; @Override public SocialContentItemResponse manualImport(String userId, SocialContentManualImportRequest request) { validateUser(userId); String platform = normalizePlatform(request.getPlatform()); String content = normalizeContent(request.getContent()); SocialContentItem item = findDuplicate(userId, platform, content); if (item == null) { item = new SocialContentItem(); item.setUserId(userId); item.setPlatform(platform); item.setSourceType("manual_text"); item.setTitle(trimToNull(request.getTitle())); item.setContent(content); item.setImportStatus("parsed"); item.setApprovedForAi(Boolean.TRUE.equals(request.getApprovedForAi()) ? 1 : 0); item.setContentHash(hashContent(content)); this.save(item); } else if (Boolean.TRUE.equals(request.getApprovedForAi()) && !isApproved(item)) { item.setApprovedForAi(1); this.updateById(item); } logConsentIfApproved(userId, platform, item.getApprovedForAi()); return convertToResponse(item); } @Override public SocialContentItemResponse linkImport(String userId, SocialContentLinkImportRequest request) { validateUser(userId); String platform = normalizePlatform(request.getPlatform()); if (!StringUtils.hasText(request.getSourceUrl())) { throw new IllegalArgumentException("来源链接不能为空"); } String content = StringUtils.hasText(request.getContent()) ? normalizeContent(request.getContent()) : normalizeContent(request.getSourceUrl()); SocialContentItem item = findDuplicate(userId, platform, content); if (item == null) { item = new SocialContentItem(); item.setUserId(userId); item.setPlatform(platform); item.setSourceType("public_link"); item.setSourceUrl(request.getSourceUrl().trim()); item.setTitle(trimToNull(request.getTitle())); item.setContent(content); item.setImportStatus("parsed"); item.setApprovedForAi(Boolean.TRUE.equals(request.getApprovedForAi()) ? 1 : 0); item.setContentHash(hashContent(content)); this.save(item); } else if (Boolean.TRUE.equals(request.getApprovedForAi()) && !isApproved(item)) { item.setApprovedForAi(1); this.updateById(item); } logConsentIfApproved(userId, platform, item.getApprovedForAi()); return convertToResponse(item); } @Override public SocialContentItemResponse screenshotImport(String userId, String platform, MultipartFile file) { validateUser(userId); normalizePlatform(platform); if (file == null || file.isEmpty()) { throw new IllegalArgumentException("截图文件不能为空"); } if (file.getSize() > MAX_SCREENSHOT_SIZE) { throw new IllegalArgumentException("截图不能超过5MB"); } String name = file.getOriginalFilename() == null ? "" : file.getOriginalFilename().toLowerCase(Locale.ROOT); if (!(name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png") || name.endsWith(".webp"))) { throw new IllegalArgumentException("仅支持jpg、png、webp截图"); } throw new IllegalStateException("OCR暂未启用,请先使用粘贴文本导入"); } @Override public List listByUser(String userId) { validateUser(userId); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SocialContentItem::getUserId, userId) .eq(SocialContentItem::getIsDeleted, 0) .orderByDesc(SocialContentItem::getCreateTime); return this.list(wrapper).stream().map(this::convertToResponse).collect(Collectors.toList()); } @Override public boolean deleteByUser(String userId, String id, Boolean keepConfirmedInsights) { validateUser(userId); SocialContentItem item = getOwned(userId, id); if (item == null) { return false; } item.setIsDeleted(1); item.setDeletedAt(LocalDateTime.now()); this.updateById(item); LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(SocialProfileInsight::getUserId, userId) .eq(SocialProfileInsight::getSourceItemId, id) .eq(SocialProfileInsight::getIsDeleted, 0); if (Boolean.TRUE.equals(keepConfirmedInsights)) { wrapper.ne(SocialProfileInsight::getStatus, "confirmed"); } wrapper.set(SocialProfileInsight::getStatus, "deleted") .set(SocialProfileInsight::getIsDeleted, 1) .set(SocialProfileInsight::getDeletedAt, LocalDateTime.now()); socialProfileInsightMapper.update(null, wrapper); return true; } @Override public SocialContentItemResponse updateApproval(String userId, String id, Boolean approvedForAi) { validateUser(userId); SocialContentItem item = getOwned(userId, id); if (item == null) { return null; } item.setApprovedForAi(Boolean.TRUE.equals(approvedForAi) ? 1 : 0); this.updateById(item); logConsentIfApproved(userId, item.getPlatform(), item.getApprovedForAi()); return convertToResponse(item); } private SocialContentItem getOwned(String userId, String id) { if (!StringUtils.hasText(id)) { return null; } LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SocialContentItem::getId, id) .eq(SocialContentItem::getUserId, userId) .eq(SocialContentItem::getIsDeleted, 0); return this.getOne(wrapper, false); } private SocialContentItem findDuplicate(String userId, String platform, String content) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(SocialContentItem::getUserId, userId) .eq(SocialContentItem::getPlatform, platform) .eq(SocialContentItem::getContentHash, hashContent(content)) .eq(SocialContentItem::getIsDeleted, 0); return this.getOne(wrapper, false); } private void logConsentIfApproved(String userId, String platform, Integer approvedForAi) { if (approvedForAi == null || approvedForAi != 1) { return; } UserConsentLog log = new UserConsentLog(); log.setUserId(userId); log.setPlatform(platform); log.setConsentType("ai_profile_analysis"); log.setConsentVersion("v1"); log.setScope("imported_social_content"); log.setPurpose("用于生成可编辑的人生画像,并增强人生剧本生成"); log.setStatus("granted"); log.setGrantedAt(LocalDateTime.now()); userConsentLogMapper.insert(log); } private String normalizePlatform(String platform) { String value = platform == null ? "" : platform.trim().toLowerCase(Locale.ROOT); if (!PLATFORMS.contains(value)) { throw new IllegalArgumentException("不支持的平台"); } return value; } private String normalizeContent(String content) { if (!StringUtils.hasText(content)) { throw new IllegalArgumentException("导入内容不能为空"); } String normalized = content.replaceAll("\\s+", " ").trim(); if (normalized.length() > MAX_CONTENT_LENGTH) { throw new IllegalArgumentException("导入内容不能超过20000个字符"); } return normalized; } private String hashContent(String content) { return DigestUtils.md5DigestAsHex(content.getBytes(StandardCharsets.UTF_8)); } private boolean isApproved(SocialContentItem item) { return item.getApprovedForAi() != null && item.getApprovedForAi() == 1; } private String trimToNull(String value) { return StringUtils.hasText(value) ? value.trim() : null; } private void validateUser(String userId) { if (!StringUtils.hasText(userId)) { throw new IllegalArgumentException("用户未登录"); } } private SocialContentItemResponse convertToResponse(SocialContentItem item) { SocialContentItemResponse response = new SocialContentItemResponse(); BeanUtils.copyProperties(item, response); response.setApprovedForAi(isApproved(item)); response.setSourceDeleted(item.getIsDeleted() != null && item.getIsDeleted() == 1); if (item.getCreateTime() != null) { response.setCreateTime(item.getCreateTime().format(DATE_TIME_FORMATTER)); } if (item.getUpdateTime() != null) { response.setUpdateTime(item.getUpdateTime().format(DATE_TIME_FORMATTER)); } return response; } }