|
|
@@ -0,0 +1,220 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.entity.*;
|
|
|
+import com.etotem.cfc.mapper.*;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class ProfileRecommendService {
|
|
|
+
|
|
|
+ @Resource private MemberProfileDimensionMapper profileDimMapper;
|
|
|
+ @Resource private ArticleDimensionMappingMapper articleDimMapper;
|
|
|
+ @Resource private ActivityDimensionMappingMapper activityDimMapper;
|
|
|
+ @Resource private ProductDimensionMappingMapper productDimMapper;
|
|
|
+ @Resource private AiQSessionMapper sessionMapper;
|
|
|
+ @Resource private ArticleMapper articleMapper;
|
|
|
+ @Resource private ActivityMapper activityMapper;
|
|
|
+ @Resource private ProductMapper productMapper;
|
|
|
+ @Resource private RecommendationLogMapper recommendationLogMapper;
|
|
|
+
|
|
|
+ public Map<String, Object> recommend(Long memberId, List<String> types, int limit) {
|
|
|
+ Long sessionId = getLatestFinishedSession(memberId);
|
|
|
+ if (sessionId == null) {
|
|
|
+ return emptyResult(types, limit);
|
|
|
+ }
|
|
|
+
|
|
|
+ List<MemberProfileDimension> userDims = profileDimMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<MemberProfileDimension>()
|
|
|
+ .eq(MemberProfileDimension::getSessionId, sessionId)
|
|
|
+ .eq(MemberProfileDimension::getProfileType, "user"));
|
|
|
+ List<MemberProfileDimension> needDims = profileDimMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<MemberProfileDimension>()
|
|
|
+ .eq(MemberProfileDimension::getSessionId, sessionId)
|
|
|
+ .eq(MemberProfileDimension::getProfileType, "need"));
|
|
|
+
|
|
|
+ Set<String> needCodes = new HashSet<>();
|
|
|
+ for (MemberProfileDimension d : needDims) needCodes.add(d.getDimensionCode());
|
|
|
+
|
|
|
+ List<Map<String, Object>> results = new ArrayList<>();
|
|
|
+ int totalAcrossTypes = 0;
|
|
|
+
|
|
|
+ for (String type : types) {
|
|
|
+ List<ItemScore> items = recommendByType(memberId, type, userDims, needCodes, limit * 3);
|
|
|
+ totalAcrossTypes += items.size();
|
|
|
+ for (ItemScore item : items) {
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
+ m.put("id", item.id);
|
|
|
+ m.put("type", type);
|
|
|
+ m.put("title", item.title);
|
|
|
+ m.put("summary", item.summary);
|
|
|
+ m.put("coverImage", item.coverImage);
|
|
|
+ m.put("dimensionCode", item.dimensionCode);
|
|
|
+ m.put("recommendScore", item.score);
|
|
|
+ m.put("recommendReason", item.reason);
|
|
|
+ m.put("badge", item.badge);
|
|
|
+ results.add(m);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ results.sort((a, b) -> Double.compare((double) b.get("recommendScore"), (double) a.get("recommendScore")));
|
|
|
+ if (results.size() > limit) results = results.subList(0, limit);
|
|
|
+
|
|
|
+ for (Map<String, Object> r : results) {
|
|
|
+ logShow(memberId, (String) r.get("type"), ((Number) r.get("id")).longValue(),
|
|
|
+ ((Number) r.get("recommendScore")).doubleValue(), sessionId);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> out = new LinkedHashMap<>();
|
|
|
+ out.put("recommendations", results);
|
|
|
+ out.put("hasMore", totalAcrossTypes > limit);
|
|
|
+ out.put("profileSessionId", sessionId);
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<ItemScore> recommendByType(Long memberId, String type,
|
|
|
+ List<MemberProfileDimension> userDims,
|
|
|
+ Set<String> needCodes, int maxItems) {
|
|
|
+ if (userDims.isEmpty()) return Collections.emptyList();
|
|
|
+
|
|
|
+ double totalProfileScore = 0;
|
|
|
+ for (MemberProfileDimension d : userDims) totalProfileScore += d.getScore();
|
|
|
+ if (totalProfileScore == 0) return Collections.emptyList();
|
|
|
+
|
|
|
+ List<ItemScore> items = new ArrayList<>();
|
|
|
+
|
|
|
+ if ("product".equals(type)) {
|
|
|
+ List<ProductDimensionMapping> mappings = productDimMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<ProductDimensionMapping>()
|
|
|
+ .eq(ProductDimensionMapping::getEnabled, 1));
|
|
|
+ Map<Long, Double> scores = new HashMap<>();
|
|
|
+ for (ProductDimensionMapping m : mappings) {
|
|
|
+ double dimScore = 0;
|
|
|
+ for (MemberProfileDimension d : userDims) {
|
|
|
+ if (d.getDimensionCode().equals(m.getDimensionCode())) {
|
|
|
+ dimScore += d.getScore() * m.getMatchScore();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (dimScore > 0) {
|
|
|
+ double base = dimScore / totalProfileScore;
|
|
|
+ boolean isNeed = needCodes.contains(m.getDimensionCode());
|
|
|
+ double finalScore = base + (isNeed ? 20 : 0);
|
|
|
+ scores.merge(m.getProductId(), finalScore, Double::sum);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ scores.forEach((pid, score) -> {
|
|
|
+ Product p = productMapper.selectById(pid);
|
|
|
+ if (p != null) items.add(new ItemScore(pid, "product", p.getName(),
|
|
|
+ p.getDescription(), p.getCoverImage(), score, "高匹配"));
|
|
|
+ });
|
|
|
+ } else if ("article".equals(type)) {
|
|
|
+ List<ArticleDimensionMapping> mappings = articleDimMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<ArticleDimensionMapping>());
|
|
|
+ Map<Long, Double> scores = new HashMap<>();
|
|
|
+ for (ArticleDimensionMapping m : mappings) {
|
|
|
+ double dimScore = 0;
|
|
|
+ for (MemberProfileDimension d : userDims) {
|
|
|
+ if (d.getDimensionCode().equals(m.getDimensionCode())) {
|
|
|
+ dimScore += d.getScore() * m.getMatchScore();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (dimScore > 0) {
|
|
|
+ double base = dimScore / totalProfileScore;
|
|
|
+ boolean isNeed = needCodes.contains(m.getDimensionCode());
|
|
|
+ double finalScore = base + (isNeed ? 20 : 0);
|
|
|
+ scores.merge(m.getArticleId(), finalScore, Double::sum);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ scores.forEach((aid, score) -> {
|
|
|
+ Article a = articleMapper.selectById(aid);
|
|
|
+ if (a != null) items.add(new ItemScore(aid, "article", a.getTitle(),
|
|
|
+ a.getSummary(), a.getCoverImage(), score, "高匹配"));
|
|
|
+ });
|
|
|
+ } else if ("activity".equals(type)) {
|
|
|
+ List<ActivityDimensionMapping> mappings = activityDimMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<ActivityDimensionMapping>());
|
|
|
+ Map<Long, Double> scores = new HashMap<>();
|
|
|
+ for (ActivityDimensionMapping m : mappings) {
|
|
|
+ double dimScore = 0;
|
|
|
+ for (MemberProfileDimension d : userDims) {
|
|
|
+ if (d.getDimensionCode().equals(m.getDimensionCode())) {
|
|
|
+ dimScore += d.getScore() * m.getMatchScore();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (dimScore > 0) {
|
|
|
+ double base = dimScore / totalProfileScore;
|
|
|
+ boolean isNeed = needCodes.contains(m.getDimensionCode());
|
|
|
+ double finalScore = base + (isNeed ? 20 : 0);
|
|
|
+ scores.merge(m.getActivityId(), finalScore, Double::sum);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ scores.forEach((actId, score) -> {
|
|
|
+ Activity act = activityMapper.selectById(actId);
|
|
|
+ if (act != null) items.add(new ItemScore(actId, "activity", act.getTitle(),
|
|
|
+ act.getDescription(), act.getCoverImage(), score, "高匹配"));
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ items.sort((a, b) -> Double.compare(b.score, a.score));
|
|
|
+ return items.size() > maxItems ? items.subList(0, maxItems) : items;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Long getLatestFinishedSession(Long memberId) {
|
|
|
+ AiQSession s = sessionMapper.selectOne(new LambdaQueryWrapper<AiQSession>()
|
|
|
+ .eq(AiQSession::getMemberId, memberId)
|
|
|
+ .eq(AiQSession::getStatus, "finished")
|
|
|
+ .orderByDesc(AiQSession::getFinishedAt)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ return s != null ? s.getId() : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> emptyResult(List<String> types, int limit) {
|
|
|
+ Map<String, Object> out = new LinkedHashMap<>();
|
|
|
+ out.put("recommendations", Collections.emptyList());
|
|
|
+ out.put("hasMore", false);
|
|
|
+ out.put("profileSessionId", null);
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void logShow(Long memberId, String type, Long contentId, double score, Long sessionId) {
|
|
|
+ try {
|
|
|
+ RecommendationLog log = new RecommendationLog();
|
|
|
+ log.setUserId(memberId);
|
|
|
+ log.setContentType(type);
|
|
|
+ log.setContentId(contentId);
|
|
|
+ log.setRecommendScore((int) score);
|
|
|
+ log.setAction("show");
|
|
|
+ log.setRecommendType("profile");
|
|
|
+ recommendationLogMapper.insert(log);
|
|
|
+ } catch (Exception e) {
|
|
|
+ // log silently
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static class ItemScore {
|
|
|
+ final long id;
|
|
|
+ final String type;
|
|
|
+ final String title;
|
|
|
+ final String summary;
|
|
|
+ final String coverImage;
|
|
|
+ final double score;
|
|
|
+ final String reason;
|
|
|
+ final String badge;
|
|
|
+ final String dimensionCode;
|
|
|
+
|
|
|
+ ItemScore(long id, String type, String title, String summary, String coverImage,
|
|
|
+ double score, String reason) {
|
|
|
+ this.id = id; this.type = type; this.title = title;
|
|
|
+ this.summary = summary; this.coverImage = coverImage;
|
|
|
+ this.score = score; this.reason = reason;
|
|
|
+ this.badge = score >= 70 ? "优秀匹配" : score >= 40 ? "一般匹配" : "低匹配";
|
|
|
+ this.dimensionCode = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|