Преглед изворни кода

feat(pearl): ResourceService 25组聚合/互动/提醒/价值分

Sisyphus Agent пре 1 недеља
родитељ
комит
705f924de7

+ 2 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/PearlController.java

@@ -27,11 +27,12 @@ public class PearlController {
     public Result<ResourceItem> addItem(@RequestBody Map<String, Object> params,
                                         @RequestAttribute("userId") Long userId) {
         String type = (String) params.get("type");
+        String connectionType = (String) params.get("connectionType");
         String name = (String) params.get("name");
         String description = (String) params.get("description");
         Long contactId = params.get("contactId") != null
                 ? Long.valueOf(params.get("contactId").toString()) : null;
-        return resourceService.addResourceItem(userId, type, name, description, contactId);
+        return resourceService.addResourceItem(userId, type, connectionType, name, description, contactId);
     }
 
     /** 删除登记的珍珠图资源 */

+ 236 - 32
cfc-backend/src/main/java/com/etotem/cfc/service/ResourceService.java

@@ -1,16 +1,24 @@
 package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.Contact;
 import com.etotem.cfc.entity.ContactHelpLog;
+import com.etotem.cfc.entity.PearlInteractionLog;
+import com.etotem.cfc.entity.PearlInteractionReminder;
 import com.etotem.cfc.entity.ResourceItem;
+import com.etotem.cfc.enums.ConnectionType;
 import com.etotem.cfc.mapper.ContactHelpLogMapper;
 import com.etotem.cfc.mapper.ContactMapper;
+import com.etotem.cfc.mapper.PearlInteractionLogMapper;
+import com.etotem.cfc.mapper.PearlInteractionReminderMapper;
 import com.etotem.cfc.mapper.ResourceItemMapper;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
@@ -34,40 +42,55 @@ public class ResourceService {
     @Resource
     private ContactHelpLogMapper helpLogMapper;
 
-    /** 珍珠图聚合:人(contacts) + 技能(help_logs SKILL) + 信息/场所(resource_items) */
+    @Resource
+    private PearlInteractionLogMapper interactionLogMapper;
+
+    @Resource
+    private PearlInteractionReminderMapper reminderMapper;
+
+    /** 25 类连接类型 → 前端选择器/渲染配置 */
+    public List<Map<String, Object>> getConnectionTypes() {
+        List<Map<String, Object>> list = new ArrayList<>();
+        for (ConnectionType t : ConnectionType.values()) {
+            Map<String, Object> m = new LinkedHashMap<>();
+            m.put("code", t.name());
+            m.put("label", t.getLabel());
+            m.put("category", t.getCategory());
+            m.put("suggestIntervalMonth", t.getSuggestIntervalMonth());
+            list.add(m);
+        }
+        return list;
+    }
+
+    /** 珍珠图聚合:25 类社会连接 + 历史资源(legacy PERSON/SKILL/INFO/PLACE) */
     public Result<Map<String, Object>> getPearlResources(Long userId) {
-        // 1. 人脉:全部联系人
+        // 历史资源:legacy 分组的 contacts(人脉)与 skillLogs(技能)照旧
         List<Contact> contacts = contactMapper.selectList(
                 new LambdaQueryWrapper<Contact>().eq(Contact::getUserId, userId)
         );
-        List<Map<String, Object>> personItems = contacts.stream().map(c -> {
-            Map<String, Object> m = new LinkedHashMap<>();
-            m.put("id", c.getId());
-            m.put("name", c.getName());
-            m.put("avatar", c.getAvatar());
-            m.put("relationshipType", c.getRelationshipType());
-            m.put("description", c.getRelationshipType());
-            return m;
-        }).collect(Collectors.toList());
-
-        // 2. 技能:SKILL 类帮助记录,按联系人聚合
         List<ContactHelpLog> skillLogs = helpLogMapper.selectList(
                 new LambdaQueryWrapper<ContactHelpLog>()
                         .eq(ContactHelpLog::getUserId, userId)
                         .eq(ContactHelpLog::getHelpType, "SKILL")
                         .orderByDesc(ContactHelpLog::getHappenedAt)
         );
-        List<Map<String, Object>> skillItems = buildSkillItems(skillLogs);
-
-        // 3+4. 信息/场所:resource_items 登记
         List<ResourceItem> resourceItems = resourceItemMapper.selectList(
                 new LambdaQueryWrapper<ResourceItem>()
                         .eq(ResourceItem::getUserId, userId)
-                        .in(ResourceItem::getType, Arrays.asList("INFO", "PLACE", "SKILL"))
                         .orderByDesc(ResourceItem::getCreatedAt)
         );
-        List<Map<String, Object>> infoItems = new ArrayList<>();
-        List<Map<String, Object>> placeItems = new ArrayList<>();
+
+        // 25 组社会连接:按 connection_type 分组
+        Map<String, List<Map<String, Object>>> connGroupMap = new LinkedHashMap<>();
+        Map<String, String> connTypeLabel = new LinkedHashMap<>();
+        for (ConnectionType t : ConnectionType.values()) {
+            connGroupMap.put(t.name(), new ArrayList<>());
+            connTypeLabel.put(t.name(), t.getLabel());
+        }
+        // 兼容历史数据:connection_type 为空的按原 type 归入 INFO/PLACE legacy
+        List<Map<String, Object>> legacyInfo = new ArrayList<>();
+        List<Map<String, Object>> legacyPlace = new ArrayList<>();
+
         for (ResourceItem item : resourceItems) {
             Map<String, Object> m = new LinkedHashMap<>();
             m.put("id", item.getId());
@@ -75,29 +98,67 @@ public class ResourceService {
             m.put("description", item.getDescription());
             m.put("contactId", item.getContactId());
             m.put("createdAt", item.getCreatedAt());
-            if ("PLACE".equals(item.getType())) {
-                placeItems.add(m);
+            m.put("valueScore", item.getValueScore() != null ? item.getValueScore() : 0);
+            m.put("priority", item.getPriority() != null ? item.getPriority() : 2);
+            m.put("interactionCount", item.getInteractionCount() != null ? item.getInteractionCount() : 0);
+            m.put("lastInteractionAt", item.getLastInteractionAt());
+            if (item.getLastInteractionAt() != null) {
+                long days = ChronoUnit.DAYS.between(
+                        item.getLastInteractionAt().toInstant(), Instant.now());
+                m.put("daysSinceLast", (int) Math.max(0, days));
+            } else {
+                m.put("daysSinceLast", null);
+            }
+            String ct = item.getConnectionType();
+            if (ct != null && !ct.isEmpty() && connGroupMap.containsKey(ct)) {
+                connGroupMap.get(ct).add(m);
             } else {
-                infoItems.add(m);
+                // 历史资源
+                if ("PLACE".equals(item.getType())) {
+                    legacyPlace.add(m);
+                } else {
+                    legacyInfo.add(m);
+                }
             }
         }
 
+        // 组装 25 组(按枚举顺序)
         List<Map<String, Object>> groups = new ArrayList<>();
-        groups.add(group("PERSON", "人脉", personItems));
-        groups.add(group("SKILL", "技能", skillItems));
-        groups.add(group("INFO", "信息", infoItems));
-        groups.add(group("PLACE", "场所", placeItems));
+        for (ConnectionType t : ConnectionType.values()) {
+            Map<String, Object> g = new LinkedHashMap<>();
+            g.put("type", t.name());
+            g.put("typeName", t.getLabel());
+            g.put("category", t.getCategory());
+            g.put("ring", "ESSENTIAL".equals(t.getCategory()) ? "inner" : "outer");
+            g.put("items", connGroupMap.get(t.name()));
+            groups.add(g);
+        }
+
+        // legacyGroups:人脉/技能/历史信息/历史场所
+        List<Map<String, Object>> legacyGroups = new ArrayList<>();
+        legacyGroups.add(group("PERSON", "人脉", contacts.stream().map(c -> {
+            Map<String, Object> m = new LinkedHashMap<>();
+            m.put("id", c.getId());
+            m.put("name", c.getName());
+            m.put("avatar", c.getAvatar());
+            m.put("relationshipType", c.getRelationshipType());
+            return m;
+        }).collect(Collectors.toList())));
+        legacyGroups.add(group("SKILL", "技能", buildSkillItems(skillLogs)));
+        legacyGroups.add(group("INFO", "信息", legacyInfo));
+        legacyGroups.add(group("PLACE", "场所", legacyPlace));
 
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("groups", groups);
+        result.put("legacyGroups", legacyGroups);
         return Result.success(result);
     }
 
-    /** 登记资源(珍珠图:信息/场所/技能类手工登记) */
-    public Result<ResourceItem> addResourceItem(Long userId, String type, String name,
-                                                String description, Long contactId) {
-        if (type == null || !Arrays.asList("INFO", "PLACE", "SKILL").contains(type)) {
-            return Result.error("资源类型不合法");
+    /** 登记资源(珍珠图:25 类社会连接,connectionType 必填) */
+    public Result<ResourceItem> addResourceItem(Long userId, String type, String connectionType,
+                                                String name, String description, Long contactId) {
+        if (connectionType == null || !ConnectionType.isValid(connectionType)) {
+            return Result.error("请选择有效的连接类型");
         }
         if (name == null || name.trim().isEmpty()) {
             return Result.error("资源名称不能为空");
@@ -111,10 +172,14 @@ public class ResourceService {
 
         ResourceItem item = new ResourceItem();
         item.setUserId(userId);
-        item.setType(type);
+        item.setType(type != null && !type.isEmpty() ? type : connectionType);
+        item.setConnectionType(connectionType);
+        item.setPriority(2);
+        item.setValueScore(50);
         item.setName(name.trim());
         item.setDescription(description != null ? description.trim() : null);
         item.setContactId(contactId);
+        item.setInteractionCount(0);
         item.setCreatedAt(new Date());
         resourceItemMapper.insert(item);
         return Result.success(item);
@@ -130,6 +195,145 @@ public class ResourceService {
         return Result.success(null);
     }
 
+    /** 记录一次互动并更新资源统计 */
+    public Result<Void> addInteraction(Long userId, Long itemId, String interactionType, String content) {
+        ResourceItem item = resourceItemMapper.selectById(itemId);
+        if (item == null || !item.getUserId().equals(userId)) {
+            return Result.error("资源不存在");
+        }
+        if (interactionType == null || !Arrays.asList("PHONE", "WECHAT", "MEETING", "OTHER").contains(interactionType)) {
+            return Result.error("互动方式不合法");
+        }
+
+        PearlInteractionLog log = new PearlInteractionLog();
+        log.setUserId(userId);
+        log.setResourceItemId(itemId);
+        log.setInteractionType(interactionType);
+        log.setContent(content != null ? content.trim() : null);
+        log.setHappenedAt(new Date());
+        log.setCreatedAt(new Date());
+        interactionLogMapper.insert(log);
+
+        item.setLastInteractionAt(log.getHappenedAt());
+        item.setInteractionCount((item.getInteractionCount() == null ? 0 : item.getInteractionCount()) + 1);
+        // 重新计算价值分(互动后价值提升)
+        List<PearlInteractionLog> logs = interactionLogMapper.selectList(
+                new LambdaQueryWrapper<PearlInteractionLog>()
+                        .eq(PearlInteractionLog::getResourceItemId, itemId)
+                        .orderByDesc(PearlInteractionLog::getHappenedAt)
+        );
+        item.setValueScore(calculateValueScore(item, logs));
+        resourceItemMapper.updateById(item);
+
+        // 关闭该资源的 PENDING 提醒
+        reminderMapper.update(null,
+                new LambdaUpdateWrapper<PearlInteractionReminder>()
+                        .eq(PearlInteractionReminder::getUserId, userId)
+                        .eq(PearlInteractionReminder::getResourceItemId, itemId)
+                        .eq(PearlInteractionReminder::getStatus, "PENDING")
+                        .set(PearlInteractionReminder::getStatus, "DONE")
+        );
+        return Result.success(null);
+    }
+
+    /** 互动历史(按时间倒序) */
+    public Result<List<Map<String, Object>>> getInteractionList(Long userId, Long itemId, int page, int size) {
+        ResourceItem item = resourceItemMapper.selectById(itemId);
+        if (item == null || !item.getUserId().equals(userId)) {
+            return Result.error("资源不存在");
+        }
+        List<PearlInteractionLog> logs = interactionLogMapper.selectList(
+                new LambdaQueryWrapper<PearlInteractionLog>()
+                        .eq(PearlInteractionLog::getResourceItemId, itemId)
+                        .orderByDesc(PearlInteractionLog::getHappenedAt)
+                        .last("LIMIT " + ((page - 1) * size) + "," + size)
+        );
+        List<Map<String, Object>> list = logs.stream().map(l -> {
+            Map<String, Object> m = new LinkedHashMap<>();
+            m.put("id", l.getId());
+            m.put("interactionType", l.getInteractionType());
+            m.put("content", l.getContent());
+            m.put("happenedAt", l.getHappenedAt());
+            return m;
+        }).collect(Collectors.toList());
+        return Result.success(list);
+    }
+
+    /** 手动覆盖优先级(1/2/3),覆盖后价值分同步调整 */
+    public Result<Void> updatePriority(Long userId, Long itemId, Integer priority) {
+        ResourceItem item = resourceItemMapper.selectById(itemId);
+        if (item == null || !item.getUserId().equals(userId)) {
+            return Result.error("资源不存在");
+        }
+        if (priority == null || priority < 1 || priority > 3) {
+            return Result.error("优先级不合法");
+        }
+        item.setPriority(priority);
+        // 手动覆盖时同步调整 valueScore 到一个合理档位
+        item.setValueScore(priority == 1 ? 80 : priority == 2 ? 50 : 20);
+        resourceItemMapper.updateById(item);
+        return Result.success(null);
+    }
+
+    /** 待办提醒列表(PENDING) */
+    public Result<List<Map<String, Object>>> getReminders(Long userId) {
+        List<PearlInteractionReminder> reminders = reminderMapper.selectList(
+                new LambdaQueryWrapper<PearlInteractionReminder>()
+                        .eq(PearlInteractionReminder::getUserId, userId)
+                        .eq(PearlInteractionReminder::getStatus, "PENDING")
+                        .orderByDesc(PearlInteractionReminder::getDaysSinceLast)
+        );
+        Map<Long, ResourceItem> itemMap = new HashMap<>();
+        List<Long> ids = reminders.stream().map(PearlInteractionReminder::getResourceItemId).collect(Collectors.toList());
+        if (!ids.isEmpty()) {
+            resourceItemMapper.selectBatchIds(ids).forEach(i -> itemMap.put(i.getId(), i));
+        }
+        List<Map<String, Object>> list = reminders.stream().map(r -> {
+            Map<String, Object> m = new LinkedHashMap<>();
+            m.put("id", r.getId());
+            m.put("resourceItemId", r.getResourceItemId());
+            ResourceItem ri = itemMap.get(r.getResourceItemId());
+            m.put("name", ri != null ? ri.getName() : "");
+            m.put("connectionType", r.getConnectionType());
+            m.put("suggestIntervalMonth", r.getSuggestIntervalMonth());
+            m.put("daysSinceLast", r.getDaysSinceLast());
+            return m;
+        }).collect(Collectors.toList());
+        return Result.success(list);
+    }
+
+    /** 自动计算价值分 0-100 */
+    public int calculateValueScore(ResourceItem item, List<PearlInteractionLog> logs) {
+        int score = 0;
+        // 1. 类型基础权重
+        ConnectionType ct = null;
+        for (ConnectionType t : ConnectionType.values()) {
+            if (t.name().equals(item.getConnectionType())) { ct = t; break; }
+        }
+        score += (ct != null && "ESSENTIAL".equals(ct.getCategory())) ? 30 : 20;
+        // 2. 近90天互动次数 × 5,上限 30
+        Date now = new Date();
+        Date ninetyDaysAgo = new Date(now.getTime() - 90L * 24 * 3600 * 1000);
+        int recent = 0;
+        if (logs != null) {
+            for (PearlInteractionLog l : logs) {
+                if (l.getHappenedAt() != null && l.getHappenedAt().after(ninetyDaysAgo)) recent++;
+            }
+        }
+        score += Math.min(recent * 5, 30);
+        // 3. 最近互动时效性
+        if (item.getLastInteractionAt() != null) {
+            long days = ChronoUnit.DAYS.between(item.getLastInteractionAt().toInstant(), Instant.now());
+            if (days <= 30) score += 20;
+            else if (days <= 90) score += 10;
+            else if (days <= 180) score += 5;
+        }
+        // 4. 互动总量 × 2,上限 20
+        int count = item.getInteractionCount() == null ? 0 : item.getInteractionCount();
+        score += Math.min(count * 2, 20);
+        return Math.min(score, 100);
+    }
+
     /** 能力图聚合:SKILL 类帮助记录按联系人聚合为能力节点 */
     public Result<List<Map<String, Object>>> getAbilityMap(Long userId) {
         List<ContactHelpLog> skillLogs = helpLogMapper.selectList(