Jelajahi Sumber

docs: 珍珠图社会连接盘点系统实现计划

Sisyphus Agent 1 Minggu lalu
induk
melakukan
319363ccca
1 mengubah file dengan 1803 tambahan dan 0 penghapusan
  1. 1803 0
      docs/superpowers/plans/2026-09-10-pearl-diagram-redesign.md

+ 1803 - 0
docs/superpowers/plans/2026-09-10-pearl-diagram-redesign.md

@@ -0,0 +1,1803 @@
+# 珍珠图社会连接盘点系统 实现计划
+
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
+
+**目标:** 将现有珍珠图从「4 组资源清单」重构为「25 类社会连接盘点系统」:两层同心圆布局(内圈必备 12 组 / 外圈理想 13 组)、珍珠大小按价值分级、互动日志 + 定期提醒。
+
+**架构:** 后端新增 `ConnectionType` 枚举 + `pearl_interaction_log`/`pearl_interaction_reminder` 两张表 + 价值分自动计算 + 每日定时提醒任务;前端扩展 `PearlDiagram.vue` 为两层同心圆布局(珍珠半径按 priority/valueScore 映射),新增 25 类类型选择器和提醒列表页。旧 PERSON/SKILL/INFO/PLACE 数据保留为 legacyGroups 不迁移。
+
+**技术栈:** Spring Boot 2.7.18 + MyBatis-Plus + Java 8;uni-app 微信小程序 (Vue 2 Options API, Canvas 2D);DatabaseInitializer 迁移
+
+---
+
+## 文件结构
+
+**后端(cfc-backend):**
+
+| 文件 | 职责 | 动作 |
+|------|------|------|
+| `src/main/java/com/etotem/cfc/enums/ConnectionType.java` | 25 类连接类型枚举(code/label/category/建议周期) | 创建 |
+| `src/main/java/com/etotem/cfc/entity/PearlInteractionLog.java` | 互动日志实体 | 创建 |
+| `src/main/java/com/etotem/cfc/entity/PearlInteractionReminder.java` | 提醒实体 | 创建 |
+| `src/main/java/com/etotem/cfc/mapper/PearlInteractionLogMapper.java` | 互动日志 Mapper | 创建 |
+| `src/main/java/com/etotem/cfc/mapper/PearlInteractionReminderMapper.java` | 提醒 Mapper | 创建 |
+| `src/main/java/com/etotem/cfc/entity/ResourceItem.java` | 扩展新字段(connectionType/priority/valueScore/lastInteractionAt/interactionCount) | 修改 |
+| `src/main/java/com/etotem/cfc/service/ResourceService.java` | getPearlResources 改为 25 组 + legacyGroups;新增互动/提醒/优先级/价值分方法 | 修改 |
+| `src/main/java/com/etotem/cfc/controller/PearlController.java` | 新增 5 个接口 | 修改 |
+| `src/main/java/com/etotem/cfc/task/PearlReminderScheduledTask.java` | 每日 08:00 生成提醒 | 创建 |
+| `src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` | 迁移 304-306(加列 + 建两表) | 修改 |
+| `src/main/resources/schema.sql` | 同步 resource_items 新字段 + 两张新表 | 修改 |
+
+**前端(cfc-frontend):**
+
+| 文件 | 职责 | 动作 |
+|------|------|------|
+| `utils/api.js` | 新增 5 个 API 方法 | 修改 |
+| `components/PearlDiagram.vue` | 两层同心圆布局 + 珍珠半径按价值映射 + 逾期角标 + 弹窗联系/优先级 | 修改 |
+| `pages/action-detail/pearl-add-resource.vue` | 类型选择改为 25 类分组选择器 | 修改 |
+| `pages/pearl-reminders/index.vue` | 提醒列表页 | 创建 |
+| `pages.json` | 注册提醒页路由 | 修改 |
+
+**文档:**
+
+| 文件 | 职责 | 动作 |
+|------|------|------|
+| `docs/superpowers/api/API_REFERENCE.md` | §4.39 同步新增接口 | 修改 |
+
+---
+
+## 任务 1:数据库迁移(迁移 304-306)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(`runMigrations()` 末尾,迁移 303 块之后)
+- 修改:`cfc-backend/src/main/resources/schema.sql`(同步)
+
+- [ ] **步骤 1:确认现有迁移 303 位置**
+
+阅读 `DatabaseInitializer.java` 末尾(约 10508-10543 行),确认 `// 迁移303` 块和 `runMigrations()` 的收尾结构。找到 `createResourceItemsTable()` 方法(或内联的 CREATE TABLE resource_items 块)。
+
+- [ ] **步骤 2:添加迁移 304(resource_items 加列)**
+
+在迁移 303 块之后、方法结束之前,追加:
+
+```java
+// 迁移304: resource_items 表添加社会连接分类字段(珍珠图重构:25 类连接类型 + 价值分级)
+try {
+    jdbcTemplate.execute("ALTER TABLE resource_items ADD COLUMN connection_type VARCHAR(50) NOT NULL DEFAULT '' COMMENT '连接类型枚举名(ConnectionType)'");
+} catch (Exception e) {
+    log.warn("迁移304: resource_items.connection_type 已存在或添加失败: {}", e.getMessage());
+}
+try {
+    jdbcTemplate.execute("ALTER TABLE resource_items ADD COLUMN priority TINYINT DEFAULT 2 COMMENT '优先级: 1=高 2=中 3=低(手动覆盖)'");
+} catch (Exception e) {
+    log.warn("迁移304: resource_items.priority 已存在或添加失败: {}", e.getMessage());
+}
+try {
+    jdbcTemplate.execute("ALTER TABLE resource_items ADD COLUMN value_score INT DEFAULT 0 COMMENT '价值分 0-100(自动计算,可手动覆盖)'");
+} catch (Exception e) {
+    log.warn("迁移304: resource_items.value_score 已存在或添加失败: {}", e.getMessage());
+}
+try {
+    jdbcTemplate.execute("ALTER TABLE resource_items ADD COLUMN last_interaction_at DATETIME DEFAULT NULL COMMENT '最近互动时间'");
+} catch (Exception e) {
+    log.warn("迁移304: resource_items.last_interaction_at 已存在或添加失败: {}", e.getMessage());
+}
+try {
+    jdbcTemplate.execute("ALTER TABLE resource_items ADD COLUMN interaction_count INT DEFAULT 0 COMMENT '互动总次数'");
+} catch (Exception e) {
+    log.warn("迁移304: resource_items.interaction_count 已存在或添加失败: {}", e.getMessage());
+}
+try {
+    jdbcTemplate.execute("ALTER TABLE resource_items ADD INDEX idx_ri_connection_type (connection_type)");
+} catch (Exception e) {
+    log.warn("迁移304: resource_items.connection_type 索引已存在或添加失败: {}", e.getMessage());
+}
+log.info("迁移304: resource_items 社会连接字段添加完成");
+```
+
+- [ ] **步骤 3:添加迁移 305(创建 pearl_interaction_log 表)**
+
+```java
+// 迁移305: 创建珍珠图互动日志表
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS pearl_interaction_log (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "user_id BIGINT NOT NULL COMMENT '操作人', " +
+            "resource_item_id BIGINT NOT NULL COMMENT '关联资源', " +
+            "interaction_type VARCHAR(20) NOT NULL COMMENT 'PHONE/WECHAT/MEETING/OTHER', " +
+            "content VARCHAR(500) DEFAULT NULL COMMENT '互动内容简记', " +
+            "happened_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '互动发生时间', " +
+            "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
+            "INDEX idx_pil_user (user_id), " +
+            "INDEX idx_pil_resource (resource_item_id), " +
+            "INDEX idx_pil_happened (happened_at) " +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='珍珠图互动日志'");
+    log.info("迁移305: 已创建 pearl_interaction_log 表");
+} catch (Exception e) {
+    log.warn("迁移305: 创建 pearl_interaction_log 表失败: {}", e.getMessage());
+}
+```
+
+- [ ] **步骤 4:添加迁移 306(创建 pearl_interaction_reminder 表)**
+
+```java
+// 迁移306: 创建珍珠图互动提醒表
+try {
+    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS pearl_interaction_reminder (" +
+            "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+            "user_id BIGINT NOT NULL, " +
+            "resource_item_id BIGINT NOT NULL, " +
+            "connection_type VARCHAR(50) NOT NULL, " +
+            "suggest_interval_month INT NOT NULL COMMENT '建议互动周期(月)', " +
+            "last_interaction_at DATETIME DEFAULT NULL, " +
+            "days_since_last INT NOT NULL COMMENT '距上次互动天数', " +
+            "status VARCHAR(20) DEFAULT 'PENDING' COMMENT 'PENDING/DONE/DISMISSED', " +
+            "created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
+            "updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+            "INDEX idx_pir_user_status (user_id, status), " +
+            "INDEX idx_pir_resource (resource_item_id) " +
+            ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='珍珠图互动提醒'");
+    log.info("迁移306: 已创建 pearl_interaction_reminder 表");
+} catch (Exception e) {
+    log.warn("迁移306: 创建 pearl_interaction_reminder 表失败: {}", e.getMessage());
+}
+```
+
+- [ ] **步骤 5:同步 schema.sql**
+
+在 `schema.sql` 的 `resource_items` 表定义(约 5251-5261 行)中同步新字段:
+
+```sql
+-- 珍珠图资源登记表(社会连接资源)
+CREATE TABLE IF NOT EXISTS resource_items (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '资源拥有者(当前用户)',
+    type VARCHAR(16) NOT NULL COMMENT '资源类型: INFO(信息)/PLACE(场所)/SKILL(技能)',
+    connection_type VARCHAR(50) NOT NULL DEFAULT '' COMMENT '连接类型枚举名(ConnectionType)',
+    priority TINYINT DEFAULT 2 COMMENT '优先级: 1=高 2=中 3=低(手动覆盖)',
+    value_score INT DEFAULT 0 COMMENT '价值分 0-100(自动计算,可手动覆盖)',
+    last_interaction_at DATETIME DEFAULT NULL COMMENT '最近互动时间',
+    interaction_count INT DEFAULT 0 COMMENT '互动总次数',
+    name VARCHAR(100) NOT NULL COMMENT '资源名称',
+    description VARCHAR(255) COMMENT '资源描述(可调用方式/说明)',
+    contact_id BIGINT DEFAULT NULL COMMENT '关联联系人ID(可选)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_ri_user (user_id),
+    INDEX idx_ri_connection_type (connection_type)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='珍珠图资源登记表(社会连接资源)';
+```
+
+在 schema.sql 末尾(或 resource_items 表之后)追加两张新表的定义:
+
+```sql
+-- 珍珠图互动日志表
+CREATE TABLE IF NOT EXISTS pearl_interaction_log (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '操作人',
+    resource_item_id BIGINT NOT NULL COMMENT '关联资源',
+    interaction_type VARCHAR(20) NOT NULL COMMENT 'PHONE/WECHAT/MEETING/OTHER',
+    content VARCHAR(500) DEFAULT NULL COMMENT '互动内容简记',
+    happened_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '互动发生时间',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    INDEX idx_pil_user (user_id),
+    INDEX idx_pil_resource (resource_item_id),
+    INDEX idx_pil_happened (happened_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='珍珠图互动日志';
+
+-- 珍珠图互动提醒表
+CREATE TABLE IF NOT EXISTS pearl_interaction_reminder (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL,
+    resource_item_id BIGINT NOT NULL,
+    connection_type VARCHAR(50) NOT NULL,
+    suggest_interval_month INT NOT NULL COMMENT '建议互动周期(月)',
+    last_interaction_at DATETIME DEFAULT NULL,
+    days_since_last INT NOT NULL COMMENT '距上次互动天数',
+    status VARCHAR(20) DEFAULT 'PENDING' COMMENT 'PENDING/DONE/DISMISSED',
+    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_pir_user_status (user_id, status),
+    INDEX idx_pir_resource (resource_item_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='珍珠图互动提醒';
+```
+
+- [ ] **步骤 6:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS,无编译错误。
+
+- [ ] **步骤 7:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql
+git commit -m "feat(pearl): 数据库迁移 304-306 社会连接字段与互动/提醒表"
+```
+
+---
+
+## 任务 2:ConnectionType 枚举 + 新实体 + Mapper
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/enums/ConnectionType.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/PearlInteractionLog.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/PearlInteractionReminder.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/PearlInteractionLogMapper.java`
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/PearlInteractionReminderMapper.java`
+
+- [ ] **步骤 1:创建 ConnectionType 枚举**
+
+```java
+package com.etotem.cfc.enums;
+
+/**
+ * 珍珠图社会连接类型枚举
+ * category: ESSENTIAL(必备,内圈) / IDEAL(理想扩展,外圈)
+ */
+public enum ConnectionType {
+
+    // 必备社会连接(ESSENTIAL)— 内圈,12 类
+    MEDICAL("医疗", "ESSENTIAL", 3),
+    EDUCATION("教育", "ESSENTIAL", 6),
+    PUBLIC_SECURITY("公安", "ESSENTIAL", 12),
+    AUTO("汽车相关", "ESSENTIAL", 6),
+    REAL_ESTATE("房产服务", "ESSENTIAL", 12),
+    TICKETING("票务", "ESSENTIAL", 12),
+    HANDYMAN("多能工人", "ESSENTIAL", 6),
+    APPLIANCE("家电", "ESSENTIAL", 12),
+    CATERING("餐饮", "ESSENTIAL", 3),
+    FOOD("食品", "ESSENTIAL", 3),
+    LEGAL("法律", "ESSENTIAL", 12),
+    WEEKEND_EXPERT("周末达人", "ESSENTIAL", 1),
+
+    // 理想社会连接扩展(IDEAL)— 外圈,13 类
+    GOVERNMENT("政府综合", "IDEAL", 12),
+    TAX("税务", "IDEAL", 12),
+    BUSINESS_ADMIN("工商", "IDEAL", 12),
+    BANK("银行", "IDEAL", 6),
+    MEDIA("媒体", "IDEAL", 12),
+    TRAVEL("旅游", "IDEAL", 12),
+    LEADING_ENTERPRISE("领军企业", "IDEAL", 12),
+    INDUSTRY_BENCHMARK("行业标杆", "IDEAL", 12),
+    OVERSEAS("国外", "IDEAL", 12),
+    UNIVERSITY("高校", "IDEAL", 12),
+    FINANCE("金融", "IDEAL", 12),
+    SENIOR_LOCAL("资深土著", "IDEAL", 6),
+    KEY_CITY("北上广等关键城市", "IDEAL", 12);
+
+    private final String label;
+    private final String category;
+    private final int suggestIntervalMonth;
+
+    ConnectionType(String label, String category, int suggestIntervalMonth) {
+        this.label = label;
+        this.category = category;
+        this.suggestIntervalMonth = suggestIntervalMonth;
+    }
+
+    public String getLabel() { return label; }
+    public String getCategory() { return category; }
+    public int getSuggestIntervalMonth() { return suggestIntervalMonth; }
+
+    /** 是否为有效枚举名(addResourceItem 校验用) */
+    public static boolean isValid(String name) {
+        if (name == null || name.isEmpty()) return false;
+        for (ConnectionType t : values()) {
+            if (t.name().equals(name)) return true;
+        }
+        return false;
+    }
+}
+```
+
+- [ ] **步骤 2:创建 PearlInteractionLog 实体**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("pearl_interaction_log")
+public class PearlInteractionLog implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private Long resourceItemId;
+
+    /** PHONE/WECHAT/MEETING/OTHER */
+    private String interactionType;
+
+    private String content;
+
+    private Date happenedAt;
+
+    private Date createdAt;
+}
+```
+
+- [ ] **步骤 3:创建 PearlInteractionReminder 实体**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("pearl_interaction_reminder")
+public class PearlInteractionReminder implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;
+
+    private Long resourceItemId;
+
+    private String connectionType;
+
+    private Integer suggestIntervalMonth;
+
+    private Date lastInteractionAt;
+
+    private Integer daysSinceLast;
+
+    /** PENDING/DONE/DISMISSED */
+    private String status;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}
+```
+
+- [ ] **步骤 4:创建两个 Mapper**
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.PearlInteractionLog;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface PearlInteractionLogMapper extends BaseMapper<PearlInteractionLog> {
+}
+```
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.PearlInteractionReminder;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface PearlInteractionReminderMapper extends BaseMapper<PearlInteractionReminder> {
+}
+```
+
+- [ ] **步骤 5:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS。
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/enums/ cfc-backend/src/main/java/com/etotem/cfc/entity/PearlInteraction*.java cfc-backend/src/main/java/com/etotem/cfc/mapper/PearlInteraction*Mapper.java
+git commit -m "feat(pearl): ConnectionType 枚举与互动/提醒实体 Mapper"
+```
+
+---
+
+## 任务 3:扩展 ResourceItem 实体
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/ResourceItem.java`
+
+- [ ] **步骤 1:添加新字段**
+
+在 `ResourceItem.java` 的 `createdAt` 之前添加:
+
+```java
+    /** 连接类型枚举名(ConnectionType),空串=历史资源 */
+    private String connectionType;
+
+    /** 优先级: 1=高 2=中 3=低(手动覆盖用) */
+    private Integer priority;
+
+    /** 价值分 0-100(自动计算,可手动覆盖) */
+    private Integer valueScore;
+
+    /** 最近互动时间 */
+    private Date lastInteractionAt;
+
+    /** 互动总次数 */
+    private Integer interactionCount;
+```
+
+- [ ] **步骤 2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS。
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/ResourceItem.java
+git commit -m "feat(pearl): ResourceItem 实体新增社会连接字段"
+```
+
+---
+
+## 任务 4:ResourceService 扩展(核心业务逻辑)
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ResourceService.java`
+
+- [ ] **步骤 1:注入新 Mapper + 添加常量/辅助方法**
+
+在 `ResourceService.java` 顶部添加注入和导入:
+
+```java
+import com.etotem.cfc.entity.PearlInteractionLog;
+import com.etotem.cfc.entity.PearlInteractionReminder;
+import com.etotem.cfc.enums.ConnectionType;
+import com.etotem.cfc.mapper.PearlInteractionLogMapper;
+import com.etotem.cfc.mapper.PearlInteractionReminderMapper;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.stream.Collectors;
+```
+
+在 `helpLogMapper` 注入之后添加:
+
+```java
+    @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;
+    }
+```
+
+- [ ] **步骤 2:重写 getPearlResources 为 25 组 + legacyGroups**
+
+替换现有 `getPearlResources` 方法体(保留 contacts/skillLogs/resourceItems 查询逻辑,输出结构改造):
+
+```java
+    /** 珍珠图聚合:25 类社会连接 + 历史资源(legacy PERSON/SKILL/INFO/PLACE) */
+    public Result<Map<String, Object>> getPearlResources(Long userId) {
+        // 历史资源:legacy 分组的 contacts(人脉)与 skillLogs(技能)照旧
+        List<Contact> contacts = contactMapper.selectList(
+                new LambdaQueryWrapper<Contact>().eq(Contact::getUserId, userId)
+        );
+        List<ContactHelpLog> skillLogs = helpLogMapper.selectList(
+                new LambdaQueryWrapper<ContactHelpLog>()
+                        .eq(ContactHelpLog::getUserId, userId)
+                        .eq(ContactHelpLog::getHelpType, "SKILL")
+                        .orderByDesc(ContactHelpLog::getHappenedAt)
+        );
+        List<ResourceItem> resourceItems = resourceItemMapper.selectList(
+                new LambdaQueryWrapper<ResourceItem>()
+                        .eq(ResourceItem::getUserId, userId)
+                        .orderByDesc(ResourceItem::getCreatedAt)
+        );
+
+        // 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<>();
+        List<Long> itemIds = resourceItems.stream().map(ResourceItem::getId).collect(Collectors.toList());
+        // 批量取互动日志用于计算 daysSinceLast
+        Map<Long, List<PearlInteractionLog>> logMap = new HashMap<>();
+        if (!itemIds.isEmpty()) {
+            interactionLogMapper.selectList(
+                    new LambdaQueryWrapper<PearlInteractionLog>()
+                            .in(PearlInteractionLog::getResourceItemId, itemIds)
+                            .orderByDesc(PearlInteractionLog::getHappenedAt)
+            ).forEach(l -> logMap.computeIfAbsent(l.getResourceItemId(), k -> new ArrayList<>()).add(l));
+        }
+
+        for (ResourceItem item : resourceItems) {
+            Map<String, Object> m = new LinkedHashMap<>();
+            m.put("id", item.getId());
+            m.put("name", item.getName());
+            m.put("description", item.getDescription());
+            m.put("contactId", item.getContactId());
+            m.put("createdAt", item.getCreatedAt());
+            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 {
+                // 历史资源
+                if ("PLACE".equals(item.getType())) {
+                    legacyPlace.add(m);
+                } else {
+                    legacyInfo.add(m);
+                }
+            }
+        }
+
+        // 组装 25 组(按枚举顺序)
+        List<Map<String, Object>> groups = new ArrayList<>();
+        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);
+    }
+```
+
+> 注意:`logMap` 在本方法中暂用于后续扩展,若编译器报未使用可删除该变量声明(保留查询有成本,建议删除:`logMap` 在此任务中不消费,删除以免死代码。详见步骤 2 说明)。
+
+- [ ] **步骤 3:addResourceItem 支持 connection_type**
+
+修改 `addResourceItem` 方法签名与校验:
+
+```java
+    /** 登记资源(珍珠图: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("资源名称不能为空");
+        }
+        if (contactId != null) {
+            Contact contact = contactMapper.selectById(contactId);
+            if (contact == null || !contact.getUserId().equals(userId)) {
+                return Result.error("关联联系人不存在");
+            }
+        }
+
+        ResourceItem item = new ResourceItem();
+        item.setUserId(userId);
+        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);
+    }
+```
+
+- [ ] **步骤 4:新增互动/提醒/优先级/价值分方法**
+
+在 `deleteResourceItem` 方法之后追加:
+
+```java
+    /** 记录一次互动并更新资源统计 */
+    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 LambdaQueryWrapper<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 item = itemMap.get(r.getResourceItemId());
+            m.put("name", item != null ? item.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);
+    }
+```
+
+- [ ] **步骤 5:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS。
+
+- [ ] **步骤 6:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ResourceService.java
+git commit -m "feat(pearl): ResourceService 25 组聚合/互动/提醒/价值分"
+```
+
+---
+
+## 任务 5:PearlController 新增接口
+
+**文件:**
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/PearlController.java`
+
+- [ ] **步骤 1:更新 addItem 签名 + 新增 5 个接口**
+
+将现有 `addItem` 方法参数顺序调整并新增接口:
+
+```java
+    /** 登记珍珠图资源(connectionType 必填,type 兼容保留) */
+    @PostMapping("/item/add")
+    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, connectionType, name, description, contactId);
+    }
+
+    /** 获取 25 类连接类型列表 */
+    @PostMapping("/connection-types")
+    public Result<List<Map<String, Object>>> connectionTypes() {
+        return Result.success(resourceService.getConnectionTypes());
+    }
+
+    /** 记录互动 */
+    @PostMapping("/interaction/add")
+    public Result<Void> addInteraction(@RequestBody Map<String, Object> params,
+                                       @RequestAttribute("userId") Long userId) {
+        Long itemId = params.get("itemId") != null
+                ? Long.valueOf(params.get("itemId").toString()) : null;
+        String interactionType = (String) params.get("interactionType");
+        String content = (String) params.get("content");
+        return resourceService.addInteraction(userId, itemId, interactionType, content);
+    }
+
+    /** 互动历史 */
+    @PostMapping("/interaction/list")
+    public Result<List<Map<String, Object>>> interactionList(@RequestBody Map<String, Object> params,
+                                                             @RequestAttribute("userId") Long userId) {
+        Long itemId = params.get("itemId") != null
+                ? Long.valueOf(params.get("itemId").toString()) : null;
+        int page = params.get("page") != null ? Integer.parseInt(params.get("page").toString()) : 1;
+        int size = params.get("size") != null ? Integer.parseInt(params.get("size").toString()) : 5;
+        return resourceService.getInteractionList(userId, itemId, page, size);
+    }
+
+    /** 待办提醒列表 */
+    @PostMapping("/reminder/list")
+    public Result<List<Map<String, Object>>> reminders(@RequestAttribute("userId") Long userId) {
+        return resourceService.getReminders(userId);
+    }
+
+    /** 手动覆盖优先级 */
+    @PostMapping("/item/update-priority")
+    public Result<Void> updatePriority(@RequestBody Map<String, Object> params,
+                                       @RequestAttribute("userId") Long userId) {
+        Long itemId = params.get("itemId") != null
+                ? Long.valueOf(params.get("itemId").toString()) : null;
+        Integer priority = params.get("priority") != null
+                ? Integer.valueOf(params.get("priority").toString()) : null;
+        return resourceService.updatePriority(userId, itemId, priority);
+    }
+```
+
+> 检查:`getConnectionTypes()` 需要是 public。若 `connectionTypes()` 与 `getConnectionTypes()` 命名有歧义(Spring Bean 无冲突,方法名不同即可)。同时确认 import `java.util.List` 已存在(控制器顶部已有)。
+
+- [ ] **步骤 2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS。
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/PearlController.java
+git commit -m "feat(pearl): PearlController 新增连接类型/互动/提醒/优先级接口"
+```
+
+---
+
+## 任务 6:定时提醒任务
+
+**文件:**
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/task/PearlReminderScheduledTask.java`
+
+- [ ] **步骤 1:创建定时任务类**
+
+```java
+package com.etotem.cfc.task;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.PearlInteractionReminder;
+import com.etotem.cfc.entity.ResourceItem;
+import com.etotem.cfc.enums.ConnectionType;
+import com.etotem.cfc.mapper.PearlInteractionReminderMapper;
+import com.etotem.cfc.mapper.ResourceItemMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.List;
+
+@Slf4j
+@Component
+public class PearlReminderScheduledTask {
+
+    @Resource
+    private ResourceItemMapper resourceItemMapper;
+
+    @Resource
+    private PearlInteractionReminderMapper reminderMapper;
+
+    /** 每日 08:00 生成珍珠图互动提醒 */
+    @Scheduled(cron = "0 0 8 * * ?")
+    public void generateReminders() {
+        log.info("===== PearlReminderScheduledTask 开始执行 =====");
+        try {
+            // 遍历所有带 connection_type 的资源
+            List<ResourceItem> items = resourceItemMapper.selectList(
+                    new LambdaQueryWrapper<ResourceItem>()
+                            .isNotNull(ResourceItem::getConnectionType)
+                            .ne(ResourceItem::getConnectionType, "")
+            );
+            int created = 0;
+            for (ResourceItem item : items) {
+                if (!ConnectionType.isValid(item.getConnectionType())) continue;
+                ConnectionType ct = null;
+                for (ConnectionType t : ConnectionType.values()) {
+                    if (t.name().equals(item.getConnectionType())) { ct = t; break; }
+                }
+                if (ct == null) continue;
+
+                int intervalDays = ct.getSuggestIntervalMonth() * 30;
+                long daysSinceLast;
+                if (item.getLastInteractionAt() == null) {
+                    // 从未互动:按创建时间起算
+                    Date base = item.getCreatedAt() != null ? item.getCreatedAt() : new Date();
+                    daysSinceLast = ChronoUnit.DAYS.between(base.toInstant(), Instant.now());
+                } else {
+                    daysSinceLast = ChronoUnit.DAYS.between(item.getLastInteractionAt().toInstant(), Instant.now());
+                }
+                if (daysSinceLast < intervalDays) continue; // 未到期
+
+                // 检查是否已有 PENDING 提醒
+                Long pendingCount = reminderMapper.selectCount(
+                        new LambdaQueryWrapper<PearlInteractionReminder>()
+                                .eq(PearlInteractionReminder::getUserId, item.getUserId())
+                                .eq(PearlInteractionReminder::getResourceItemId, item.getId())
+                                .eq(PearlInteractionReminder::getStatus, "PENDING")
+                );
+                if (pendingCount != null && pendingCount > 0) continue;
+
+                PearlInteractionReminder r = new PearlInteractionReminder();
+                r.setUserId(item.getUserId());
+                r.setResourceItemId(item.getId());
+                r.setConnectionType(item.getConnectionType());
+                r.setSuggestIntervalMonth(ct.getSuggestIntervalMonth());
+                r.setLastInteractionAt(item.getLastInteractionAt());
+                r.setDaysSinceLast((int) Math.max(0, daysSinceLast));
+                r.setStatus("PENDING");
+                r.setCreatedAt(new Date());
+                r.setUpdatedAt(new Date());
+                reminderMapper.insert(r);
+                created++;
+            }
+            log.info("珍珠图提醒生成完成,新增 {} 条", created);
+        } catch (Exception e) {
+            log.error("珍珠图提醒生成失败", e);
+        }
+        log.info("===== PearlReminderScheduledTask 执行完成 =====");
+    }
+}
+```
+
+- [ ] **步骤 2:编译验证**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS。
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/task/PearlReminderScheduledTask.java
+git commit -m "feat(pearl): 每日定时生成互动提醒任务"
+```
+
+---
+
+## 任务 7:前端 API 方法 + 路由注册
+
+**文件:**
+- 修改:`cfc-frontend/utils/api.js`(`getPearlResources` 附近,约 2277 行)
+- 修改:`cfc-frontend/pages.json`(`pearl-add-resource` 路由附近,约 1246 行)
+
+- [ ] **步骤 1:api.js 新增 5 个方法**
+
+在 `deletePearlResource` 之后追加:
+
+```javascript
+export const getConnectionTypes = () => request('/api/pearl/connection-types', 'POST')
+export const addPearlInteraction = (data) => request('/api/pearl/interaction/add', 'POST', data)
+export const getPearlInteractionList = (data) => request('/api/pearl/interaction/list', 'POST', data)
+export const getPearlReminders = () => request('/api/pearl/reminder/list', 'POST')
+export const updatePearlPriority = (data) => request('/api/pearl/item/update-priority', 'POST', data)
+```
+
+- [ ] **步骤 2:pages.json 注册提醒页路由**
+
+阅读 `pages.json` 中 `pearl-add-resource` 路由的格式(约 1246 行),在其后追加:
+
+```json
+{
+  "path": "pearl-reminders/index",
+  "style": {
+    "navigationBarTitleText": "互动提醒"
+  }
+}
+```
+
+> 注意:确认现有 pages 数组的根路径约定——`pearl-add-resource` 位于 `pages/action-detail/` 下(root: pages/action-detail)。新增页面 `pages/pearl-reminders/index.vue` 需要在 pages.json 的**主 pages 数组**(顶层 `"pages"`)中注册,路径为 `"pages/pearl-reminders/index"`,并检查是否已有同名 root 子包。若项目页面全部平铺在顶层 pages 数组,则直接追加该路径项。
+
+- [ ] **步骤 3:检查 pages.json 结构**
+
+阅读 `pages.json` 前 30 行确认顶层结构(`pages` 数组 vs 分包 `subPackages`),确保步骤 2 的插入位置正确。若存在 `subPackages` 结构且 action-detail 是分包,`pearl-reminders` 应放在同一分包或顶层 pages,遵循现有约定。
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-frontend/utils/api.js cfc-frontend/pages.json
+git commit -m "feat(pearl): 前端 API 方法与提醒页路由"
+```
+
+---
+
+## 任务 8:PearlDiagram.vue 两层同心圆重构
+
+**文件:**
+- 修改:`cfc-frontend/components/PearlDiagram.vue`
+
+> ⚠️ 小程序限制:禁止可选链 `?.`、禁止 CSS Grid、禁止 `:key` 表达式、禁止直接 `new Date(string)`。本组件全部为 Canvas 绘制 + 现有弹窗结构,遵守 Vue 2 Options API。
+
+- [ ] **步骤 1:重写 groupConfigs 为 25 组两层配置**
+
+替换 `data()` 中的 `groupConfigs`(原 4 组):
+
+```javascript
+      // 分组配置:25 类社会连接,内圈必备 12 组 / 外圈理想 13 组
+      // 颜色盘:内圈偏暖(红橙),外圈偏冷(蓝紫绿)
+      groupConfigs: [
+        // ===== 内圈:必备 12 组(radiusPct 0.28)=====
+        { type: 'MEDICAL', typeName: '医疗', color: '#EF4444', radiusPct: 0.28, ring: 'inner' },
+        { type: 'EDUCATION', typeName: '教育', color: '#F97316', radiusPct: 0.28, ring: 'inner' },
+        { type: 'PUBLIC_SECURITY', typeName: '公安', color: '#DC2626', radiusPct: 0.28, ring: 'inner' },
+        { type: 'AUTO', typeName: '汽车相关', color: '#EA580C', radiusPct: 0.28, ring: 'inner' },
+        { type: 'REAL_ESTATE', typeName: '房产服务', color: '#D97706', radiusPct: 0.28, ring: 'inner' },
+        { type: 'TICKETING', typeName: '票务', color: '#CA8A04', radiusPct: 0.28, ring: 'inner' },
+        { type: 'HANDYMAN', typeName: '多能工人', color: '#B45309', radiusPct: 0.28, ring: 'inner' },
+        { type: 'APPLIANCE', typeName: '家电', color: '#92400E', radiusPct: 0.28, ring: 'inner' },
+        { type: 'CATERING', typeName: '餐饮', color: '#E11D48', radiusPct: 0.28, ring: 'inner' },
+        { type: 'FOOD', typeName: '食品', color: '#BE123C', radiusPct: 0.28, ring: 'inner' },
+        { type: 'LEGAL', typeName: '法律', color: '#B91C1C', radiusPct: 0.28, ring: 'inner' },
+        { type: 'WEEKEND_EXPERT', typeName: '周末达人', color: '#FB7185', radiusPct: 0.28, ring: 'inner' },
+        // ===== 外圈:理想 13 组(radiusPct 0.55)=====
+        { type: 'GOVERNMENT', typeName: '政府综合', color: '#8B5CF6', radiusPct: 0.55, ring: 'outer' },
+        { type: 'TAX', typeName: '税务', color: '#7C3AED', radiusPct: 0.55, ring: 'outer' },
+        { type: 'BUSINESS_ADMIN', typeName: '工商', color: '#6D28D9', radiusPct: 0.55, ring: 'outer' },
+        { type: 'BANK', typeName: '银行', color: '#2563EB', radiusPct: 0.55, ring: 'outer' },
+        { type: 'MEDIA', typeName: '媒体', color: '#1D4ED8', radiusPct: 0.55, ring: 'outer' },
+        { type: 'TRAVEL', typeName: '旅游', color: '#0EA5E9', radiusPct: 0.55, ring: 'outer' },
+        { type: 'LEADING_ENTERPRISE', typeName: '领军企业', color: '#0891B2', radiusPct: 0.55, ring: 'outer' },
+        { type: 'INDUSTRY_BENCHMARK', typeName: '行业标杆', color: '#059669', radiusPct: 0.55, ring: 'outer' },
+        { type: 'OVERSEAS', typeName: '国外', color: '#10B981', radiusPct: 0.55, ring: 'outer' },
+        { type: 'UNIVERSITY', typeName: '高校', color: '#16A34A', radiusPct: 0.55, ring: 'outer' },
+        { type: 'FINANCE', typeName: '金融', color: '#84CC16', radiusPct: 0.55, ring: 'outer' },
+        { type: 'SENIOR_LOCAL', typeName: '资深土著', color: '#65A30D', radiusPct: 0.55, ring: 'outer' },
+        { type: 'KEY_CITY', typeName: '北上广等', color: '#4D7C0F', radiusPct: 0.55, ring: 'outer' }
+      ],
+```
+
+- [ ] **步骤 2:重写 buildLayout 为环形均分布局**
+
+替换 `buildLayout` 方法体(25 组平均分角度,内圈 12 / 外圈 13 各占一半圆周,按 `ring` 分组计算):
+
+```javascript
+    buildLayout: function() {
+      this.pearlNodes = []
+      if (!this.resources || !this.resources.groups || !Array.isArray(this.resources.groups)) {
+        return
+      }
+
+      var cx = this.canvasWidth / 2
+      var cy = this.canvasHeight / 2
+      var minDim = Math.min(this.canvasWidth, this.canvasHeight)
+      var self = this
+
+      // 按 ring 分组计算角度:内圈从 -PI/2(顶) 顺时针一圈,外圈同理错开 1/2 组距
+      var innerConfigs = []
+      var outerConfigs = []
+      for (var c = 0; c < this.groupConfigs.length; c++) {
+        var cfg = this.groupConfigs[c]
+        if (cfg.ring === 'inner') innerConfigs.push(cfg)
+        else outerConfigs.push(cfg)
+      }
+
+      function placeRing(ringConfigs, radiusPct) {
+        var R = minDim * radiusPct
+        var n = ringConfigs.length
+        var totalSpan = Math.PI * 2
+        var gap = totalSpan * 0.015 // 组间间隙
+        var used = 0
+        for (var i = 0; i < n; i++) {
+          var config = ringConfigs[i]
+          // 在 resources.groups 中查找该类型
+          var group = null
+          for (var g = 0; g < self.resources.groups.length; g++) {
+            if (self.resources.groups[g].type === config.type) {
+              group = self.resources.groups[g]
+              break
+            }
+          }
+          if (!group || !group.items || group.items.length === 0) {
+            used += (totalSpan / n)
+            continue
+          }
+
+          // 本组角度区间
+          var groupSpan = totalSpan / n - gap
+          var startAngle = -Math.PI / 2 + used + gap / 2
+          var items = group.items
+
+          for (var k = 0; k < items.length; k++) {
+            var item = items[k]
+            var t = items.length === 1 ? 0.5 : k / (items.length - 1)
+            var angle = startAngle + t * groupSpan
+
+            // 珍珠半径:按 priority / valueScore 映射(高=大)
+            var priority = item.priority || 2
+            var multiplier = priority === 1 ? 1.5 : priority === 3 ? 0.7 : 1.0
+            var baseR = minDim * radiusPct * 0.09
+            var itemRadius = Math.max(8, Math.min(26, baseR * multiplier))
+
+            self.pearlNodes.push({
+              id: item.id || item.contactId || null,
+              contactId: item.contactId || null,
+              groupType: config.type,
+              groupTypeName: config.typeName,
+              name: item.name || '未知',
+              displayName: item.name ? item.name.charAt(0) : '?',
+              avatar: item.avatar || null,
+              description: item.description || '',
+              skillCount: item.skillCount || 0,
+              skills: item.skills || [],
+              relationshipType: item.relationshipType || '',
+              priority: priority,
+              valueScore: item.valueScore || 0,
+              daysSinceLast: item.daysSinceLast || null,
+              radius: itemRadius,
+              color: config.color,
+              ring: config.ring,
+              x: cx + R * Math.cos(angle),
+              y: cy + R * Math.sin(angle)
+            })
+          }
+          used += (totalSpan / n)
+        }
+      }
+
+      placeRing(innerConfigs, 0.28)
+      placeRing(outerConfigs, 0.55)
+    },
+```
+
+> ⚠️ 原布局逻辑:内圈/外圈每组各占半圈会导致 12+13 组分布不均。上述实现让内圈 12 组均分整个圆周、外圈 13 组均分整个圆周——两层叠加,靠半径与颜色区分。若视觉重叠严重(内外圈珍珠在同一角度),可在 placeRing 给外层加 `+ (Math.PI / n / 2)` 偏移错开。此偏移已通过 `startAngle = -PI/2 + used + gap/2` 天然错开(内外圈组数不同,`used` 步长不同)。
+
+- [ ] **步骤 3:重写绘制逻辑(drawAll 支持双层 + 逾期角标)**
+
+替换 `drawAll` 方法(增加逾期角标绘制)与 `_drawPearlNode` 的描边逻辑:
+
+```javascript
+    drawAll: function() {
+      if (!this.ctx) return
+      var ctx = this.ctx
+      var w = this.canvasWidth
+      var h = this.canvasHeight
+
+      ctx.clearRect(0, 0, w, h)
+
+      // 1. 绘制分组弧线背景(半透明色带,双层)
+      this._drawGroupArcs(ctx)
+
+      // 2. 绘制中心节点"我"
+      this._drawCenterNode(ctx)
+
+      // 3. 绘制珍珠节点(内圈先画,外圈后画)
+      for (var i = 0; i < this.pearlNodes.length; i++) {
+        this._drawPearlNode(ctx, this.pearlNodes[i])
+      }
+
+      // 4. 绘制分组标签
+      this._drawGroupLabels(ctx)
+
+      // 5. 逾期角标(红色圆点+天数,仅 daysSinceLast 超过建议周期的资源)
+      this._drawOverdueBadges(ctx)
+    },
+
+    /** 逾期角标:红色小圆点 + 天数(仅当 PENDING 提醒语义成立时,由父组件传入 overdueMap) */
+    _drawOverdueBadges: function(ctx) {
+      if (!this.overdueMap) return
+      var self = this
+      var overdueIds = Object.keys(this.overdueMap)
+      for (var i = 0; i < this.pearlNodes.length; i++) {
+        var node = this.pearlNodes[i]
+        if (!node.id) continue
+        var days = this.overdueMap['' + node.id]
+        if (days === undefined || days === null) continue
+        var bx = node.x + node.radius - 4
+        var by = node.y - node.radius + 4
+        // 红底白字小徽标
+        ctx.beginPath()
+        ctx.arc(bx, by, 9, 0, Math.PI * 2)
+        ctx.fillStyle = '#EF4444'
+        ctx.fill()
+        ctx.fillStyle = '#FFFFFF'
+        ctx.font = 'bold 10px sans-serif'
+        ctx.textAlign = 'center'
+        ctx.textBaseline = 'middle'
+        ctx.fillText(String(days), bx, by + 1)
+      }
+    },
+```
+
+- [ ] **步骤 4:更新 `_drawPearlNode` 高价值描边 + 空状态文案**
+
+在 `_drawPearlNode` 中,画完珍珠主体后追加高优先级白色描边:
+
+```javascript
+      // 高优先级(priority=1)白色粗描边
+      if (node.priority === 1) {
+        ctx.beginPath()
+        ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2)
+        ctx.lineWidth = 2
+        ctx.strokeStyle = '#FFFFFF'
+        ctx.stroke()
+      }
+```
+
+同时更新模板中空状态文案(`pearl-graph-empty` 部分):
+
+```html
+    <text class="empty-hint">登记你的社会连接,画出珍珠项链图</text>
+```
+
+- [ ] **步骤 5:添加 overdueMap prop**
+
+在 `props` 中添加:
+
+```javascript
+    /** 逾期提醒映射 { resourceItemId: daysSinceLast },父组件从 /reminder/list 构建 */
+    overdueMap: {
+      type: Object,
+      default: null
+    }
+```
+
+- [ ] **步骤 6:更新数据源解析(父组件传 25 组 + legacyGroups)**
+
+在 `action-detail/index.vue` 中,加载资源后构建 `overdueMap` 并传入组件。修改该页面的资源加载逻辑:
+
+```javascript
+      // 现有 getPearlResources 调用之后,新增:
+      var remindersRes = await getPearlReminders()
+      var overdueMap = {}
+      if (remindersRes.code === 200 && remindersRes.data) {
+        for (var i = 0; i < remindersRes.data.length; i++) {
+          var r = remindersRes.data[i]
+          overdueMap['' + r.resourceItemId] = r.daysSinceLast
+        }
+      }
+      this.overdueMap = overdueMap
+```
+
+> 若 `action-detail/index.vue` 未引入 `getPearlReminders`,需在 import 语句中添加。同时确认 `overdueMap` 作为 prop 绑定到 `<PearlDiagram :overdue-map="overdueMap" ...>`。
+
+- [ ] **步骤 7:检查弹窗交互(联系按钮 + 优先级调整)**
+
+阅读 `PearlDiagram.vue` 中弹窗部分(`selectedNode` 相关),在 `popup-actions` 中追加「联系」和「优先级」按钮(样式沿用现有 `.action-btn`):
+
+```html
+        <!-- 操作按钮 -->
+        <view class="popup-actions">
+          <text class="action-btn action-btn-contact" v-if="selectedNode.groupType !== 'PERSON' && selectedNode.groupType !== 'SKILL'" @tap="onContact">联系</text>
+          <text class="action-btn action-btn-priority" v-if="selectedNode.groupType !== 'PERSON' && selectedNode.groupType !== 'SKILL'" @tap="onAdjustPriority">优先级</text>
+          <text class="action-btn action-btn-delete" v-if="canDelete" @tap="onDelete">删除</text>
+        </view>
+```
+
+在 `methods` 中新增:
+
+```javascript
+    /** 联系:弹出方式选择 */
+    onContact: function() {
+      var self = this
+      var node = this.selectedNode
+      if (!node || !node.id) return
+      uni.showActionSheet({
+        itemList: ['电话', '微信', '见面', '其他'],
+        success: function(res) {
+          var types = ['PHONE', 'WECHAT', 'MEETING', 'OTHER']
+          uni.showModal({
+            title: '记录互动',
+            editable: true,
+            placeholderText: '简单记录这次互动(选填)',
+            success: function(mr) {
+              if (mr.confirm) {
+                var content = mr.content || ''
+                addPearlInteraction({
+                  itemId: node.id,
+                  interactionType: types[res.tapIndex],
+                  content: content
+                }).then(function(res2) {
+                  if (res2.code === 200) {
+                    uni.showToast({ title: '已记录', icon: 'success' })
+                    self.$emit('interaction-updated')
+                  } else {
+                    uni.showToast({ title: res2.message || '记录失败', icon: 'none' })
+                  }
+                })
+              }
+            }
+          })
+        }
+      })
+    },
+
+    /** 调整优先级 */
+    onAdjustPriority: function() {
+      var self = this
+      var node = this.selectedNode
+      if (!node || !node.id) return
+      uni.showActionSheet({
+        itemList: ['高(大珍珠)', '中(默认)', '低(小珍珠)'],
+        success: function(res) {
+          updatePearlPriority({
+            itemId: node.id,
+            priority: res.tapIndex + 1
+          }).then(function(res2) {
+            if (res2.code === 200) {
+              uni.showToast({ title: '已调整', icon: 'success' })
+              self.$emit('interaction-updated')
+            } else {
+              uni.showToast({ title: res2.message || '调整失败', icon: 'none' })
+            }
+          })
+        }
+      })
+    },
+```
+
+> ⚠️ `uni.showModal` 的 `editable` 参数在部分微信基础库版本可用,若目标基础库 2.25.0 不支持,改用自定义输入弹窗(或先只记录方式不记录内容)。实现时检查现有代码是否有可复用的输入弹窗组件。
+
+- [ ] **步骤 8:在组件顶部引入新 API**
+
+```javascript
+import { addPearlInteraction, updatePearlPriority } from '../utils/api.js'
+```
+
+> 注意:`PearlDiagram.vue` 位于 `components/` 下,import 路径需与现有文件(如 `utils/api.js`)的相对路径约定一致——确认现有组件如何 import api.js。
+
+- [ ] **步骤 9:编译验证(HBuilderX 或 CLI)**
+
+在 `cfc-frontend` 目录运行(若配置了 CLI):`npm run dev:mp-weixin` 或使用微信开发者工具导入,检查无编译错误。
+
+- [ ] **步骤 10:Commit**
+
+```bash
+git add cfc-frontend/components/PearlDiagram.vue cfc-frontend/pages/action-detail/index.vue
+git commit -m "feat(pearl): 珍珠图两层同心圆布局 + 价值分级 + 互动/优先级交互"
+```
+
+---
+
+## 任务 9:pearl-add-resource.vue 25 类类型选择器
+
+**文件:**
+- 修改:`cfc-frontend/pages/action-detail/pearl-add-resource.vue`
+
+- [ ] **步骤 1:改为 25 类分组选择器**
+
+替换模板中的 `resourceTypes` 网格为两段式选择(必备/理想 Tab + 类型网格):
+
+```html
+    <!-- 选择连接类型 -->
+    <view class="form-section">
+      <text class="section-label">连接类型 <text class="required-mark">*</text></text>
+      <view class="cat-tabs">
+        <view class="cat-tab" :class="{active: selectedCategory === 'ESSENTIAL'}" @click="selectedCategory = 'ESSENTIAL'">必备</view>
+        <view class="cat-tab" :class="{active: selectedCategory === 'IDEAL'}" @click="selectedCategory = 'IDEAL'">理想</view>
+      </view>
+      <view class="type-grid">
+        <view class="type-item" v-for="(t, idx) in filteredTypes" :key="idx"
+              :class="{selected: selectedType === t.code}"
+              @click="selectedType = t.code">
+          <text class="type-name">{{ t.label }}</text>
+        </view>
+      </view>
+    </view>
+```
+
+- [ ] **步骤 2:更新 script 数据与方法**
+
+```javascript
+import { getContactList, getConnectionTypes, addPearlResource } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      contacts: [],
+      selectedContactId: null,
+      selectedCategory: 'ESSENTIAL',
+      connectionTypes: [],
+      selectedType: '',
+      name: '',
+      description: ''
+    }
+  },
+  computed: {
+    canSubmit() {
+      return this.selectedType && this.name.trim()
+    },
+    filteredTypes() {
+      var self = this
+      return this.connectionTypes.filter(function(t) {
+        return t.category === self.selectedCategory
+      })
+    }
+  },
+  onLoad(options) {
+    this.loadConnectionTypes()
+    this.loadContacts()
+  },
+  methods: {
+    async loadConnectionTypes() {
+      try {
+        var res = await getConnectionTypes()
+        if (res.code === 200 && res.data) {
+          this.connectionTypes = res.data
+        }
+      } catch (e) {
+        uni.showToast({ title: '连接类型加载失败', icon: 'none' })
+      }
+    },
+    // ... 原有 loadContacts / selectContact 保留
+    async onSubmit() {
+      if (!this.canSubmit) {
+        uni.showToast({ title: '请选择类型并填写名称', icon: 'none' })
+        return
+      }
+      try {
+        var data = {
+          connectionType: this.selectedType,
+          name: this.name.trim(),
+          description: this.description.trim()
+        }
+        if (this.selectedContactId) {
+          data.contactId = this.selectedContactId
+        }
+        var res = await addPearlResource(data)
+        if (res.code === 200) {
+          uni.showToast({ title: '保存成功', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateBack()
+          }, 1500)
+        } else {
+          uni.showToast({ title: res.message || '保存失败', icon: 'none' })
+        }
+      } catch (e) {
+        uni.showToast({ title: '保存失败', icon: 'none' })
+      }
+    }
+  }
+}
+```
+
+- [ ] **步骤 3:补充 cat-tabs 样式**
+
+在 `<style scoped>` 中添加:
+
+```css
+.cat-tabs {
+  display: flex;
+  gap: 16rpx;
+  margin-bottom: 20rpx;
+}
+.cat-tab {
+  padding: 12rpx 32rpx;
+  border: 2rpx solid #e0e0e0;
+  border-radius: 999rpx;
+  font-size: 26rpx;
+  color: #666;
+  background: #fafafa;
+}
+.cat-tab.active {
+  border-color: #F97316;
+  color: #F97316;
+  background: #FFF7ED;
+}
+```
+
+- [ ] **步骤 4:Commit**
+
+```bash
+git add cfc-frontend/pages/action-detail/pearl-add-resource.vue
+git commit -m "feat(pearl): 登记资源页 25 类连接类型分组选择器"
+```
+
+---
+
+## 任务 10:提醒列表页
+
+**文件:**
+- 创建:`cfc-frontend/pages/pearl-reminders/index.vue`
+
+- [ ] **步骤 1:创建提醒列表页**
+
+```html
+<template>
+  <view class="container">
+    <view class="page-header">
+      <text class="page-title">互动提醒</text>
+      <text class="page-sub" v-if="reminders.length > 0">{{ reminders.length }} 个连接该联系了</text>
+    </view>
+
+    <view class="reminder-list" v-if="reminders.length > 0">
+      <view class="reminder-item" v-for="(r, idx) in reminders" :key="idx">
+        <view class="reminder-info">
+          <text class="reminder-name">{{ r.name }}</text>
+          <text class="reminder-tag">{{ r.connectionType }}</text>
+          <text class="reminder-days">已 {{ r.daysSinceLast }} 天未互动(建议每 {{ r.suggestIntervalMonth }} 个月)</text>
+        </view>
+        <view class="reminder-actions">
+          <text class="btn-contact" @tap="onContact(r)">去联系</text>
+          <text class="btn-later" @tap="onLater(r)">稍后</text>
+        </view>
+      </view>
+    </view>
+
+    <view class="empty" v-else>
+      <text class="empty-icon">🎉</text>
+      <text class="empty-text">所有连接都在维护中</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getPearlReminders, addPearlInteraction } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      reminders: []
+    }
+  },
+  onShow() {
+    this.loadReminders()
+  },
+  methods: {
+    async loadReminders() {
+      try {
+        var res = await getPearlReminders()
+        if (res.code === 200 && res.data) {
+          this.reminders = res.data
+        }
+      } catch (e) {
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      }
+    },
+    onContact(r) {
+      var self = this
+      uni.showActionSheet({
+        itemList: ['电话', '微信', '见面', '其他'],
+        success: function(res) {
+          var types = ['PHONE', 'WECHAT', 'MEETING', 'OTHER']
+          addPearlInteraction({
+            itemId: r.resourceItemId,
+            interactionType: types[res.tapIndex],
+            content: ''
+          }).then(function(res2) {
+            if (res2.code === 200) {
+              uni.showToast({ title: '已记录', icon: 'success' })
+              self.loadReminders()
+            } else {
+              uni.showToast({ title: res2.message || '记录失败', icon: 'none' })
+            }
+          })
+        }
+      })
+    },
+    onLater(r) {
+      // 简化:暂不支持 DISMISS,直接刷新隐藏(PENDING 保留)
+      this.loadReminders()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.container {
+  min-height: 100vh;
+  background: #f5f5f5;
+  padding: 24rpx;
+}
+.page-header {
+  padding: 20rpx 0 30rpx;
+}
+.page-title {
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+}
+.page-sub {
+  font-size: 26rpx;
+  color: #999;
+  margin-top: 8rpx;
+  display: block;
+}
+.reminder-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+.reminder-item {
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 24rpx;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.reminder-info {
+  flex: 1;
+}
+.reminder-name {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+}
+.reminder-tag {
+  font-size: 22rpx;
+  color: #F97316;
+  background: #FFF7ED;
+  border-radius: 6rpx;
+  padding: 4rpx 12rpx;
+  margin-top: 8rpx;
+  display: inline-block;
+}
+.reminder-days {
+  font-size: 24rpx;
+  color: #999;
+  display: block;
+  margin-top: 8rpx;
+}
+.reminder-actions {
+  display: flex;
+  gap: 16rpx;
+}
+.btn-contact {
+  background: #F97316;
+  color: #fff;
+  border-radius: 999rpx;
+  padding: 12rpx 28rpx;
+  font-size: 26rpx;
+}
+.btn-later {
+  border: 2rpx solid #e0e0e0;
+  color: #666;
+  border-radius: 999rpx;
+  padding: 12rpx 28rpx;
+  font-size: 26rpx;
+}
+.empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 200rpx;
+}
+.empty-icon {
+  font-size: 80rpx;
+}
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+  margin-top: 20rpx;
+}
+</style>
+```
+
+- [ ] **步骤 2:检查 import 路径**
+
+确认 `'../../utils/api.js'` 相对路径从 `pages/pearl-reminders/index.vue` 到 `utils/api.js` 正确(与 `pages/action-detail/` 下页面一致,均为 `../../utils/api.js`)。
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add cfc-frontend/pages/pearl-reminders/index.vue
+git commit -m "feat(pearl): 互动提醒列表页"
+```
+
+---
+
+## 任务 11:API 文档同步
+
+**文件:**
+- 修改:`docs/superpowers/api/API_REFERENCE.md`(§4.39,约 1093 行)
+
+- [ ] **步骤 1:更新 §4.39 接口清单**
+
+在现有珍珠图接口表(约 1103-1105 行)追加:
+
+```markdown
+| `POST /api/pearl/connection-types` | 获取 25 类社会连接类型列表(code/label/category/suggestIntervalMonth) |
+| `POST /api/pearl/interaction/add` | 记录互动(itemId/interactionType/content),更新资源价值分并关闭 PENDING 提醒 |
+| `POST /api/pearl/interaction/list` | 互动历史(itemId/page/size) |
+| `POST /api/pearl/reminder/list` | 待办提醒列表(PENDING) |
+| `POST /api/pearl/item/update-priority` | 手动覆盖优先级(itemId/priority 1-3) |
+```
+
+并更新 `/api/pearl/resources` 响应结构说明:新增 `groups`(25 组社会连接)+ `legacyGroups`(历史 PERSON/SKILL/INFO/PLACE)。
+
+- [ ] **步骤 2:更新 `item/add` 参数说明**
+
+将 `item/add` 行参数说明更新为:`connectionType 必填(25 类枚举),name 必填,contactId 可选`。
+
+- [ ] **步骤 3:Commit**
+
+```bash
+git add docs/superpowers/api/API_REFERENCE.md
+git commit -m "docs(pearl): API_REFERENCE 同步新增接口"
+```
+
+---
+
+## 任务 12:端到端验证
+
+**文件:**
+- 验证:`cfc-backend` 编译 + `cfc-frontend` 编译
+
+- [ ] **步骤 1:后端完整编译**
+
+运行:`cd cfc-backend && mvn clean compile`
+预期:BUILD SUCCESS,无 error/warning 新增。
+
+- [ ] **步骤 2:前端编译检查**
+
+在 `cfc-frontend` 下运行(若 CLI 可用):
+```bash
+npm run build:mp-weixin
+```
+或使用微信开发者工具导入项目,确认无编译报错(重点:可选链/Grid/:key 表达式等小程序限制未违反)。
+
+- [ ] **步骤 3:代码复查**
+
+运行路由重复检查(确认无新路由冲突):
+```bash
+grep -rn '@Mapping' cfc-backend/src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u
+```
+确认 `/api/pearl/*` 无重复路由。
+
+- [ ] **步骤 4:Commit(若修复了问题)**
+
+```bash
+git add -A
+git commit -m "fix(pearl): 端到端验证修复"
+```
+
+---
+
+## 自检记录
+
+**规格覆盖度:**
+- 25 类 ConnectionType(必备 12 + 理想 13)→ 任务 1(迁移)、任务 2(枚举)、任务 8(前端配置)✓
+- 两层同心圆布局(内圈必备/外圈理想)→ 任务 8 步骤 1-2 ✓
+- 珍珠大小按价值分级 → 任务 8 步骤 2(priorityMultiplier)✓
+- 系统自动价值分 + 手动覆盖 → 任务 4 步骤 4(calculateValueScore/updatePriority)✓
+- 互动日志 + 定期提醒 → 任务 4 步骤 4、任务 6(定时任务)、任务 10(提醒页)✓
+- 25 类固定不允许自定义 → 枚举固定,addResourceItem 校验 ConnectionType.isValid ✓
+- 旧数据保留为历史资源 → 任务 4 步骤 2(legacyGroups)✓
+- 数据库迁移 + schema.sql 同步 → 任务 1 ✓
+- API 文档同步 → 任务 11 ✓
+
+**占位符扫描:** 无"待定/TODO/补充细节"等占位符;所有代码块完整。
+
+**类型一致性:**
+- `addResourceItem(userId, type, connectionType, name, description, contactId)` 签名在任务 4 定义、任务 5 调用——一致 ✓
+- `calculateValueScore(ResourceItem, List<PearlInteractionLog>)` 定义与调用签名一致 ✓
+- `overdueMap` prop 在任务 8 步骤 5 定义、步骤 6 绑定、步骤 3 使用——一致 ✓
+- 前端 `addPearlInteraction`/`updatePearlPriority`/`getPearlReminders`/`getConnectionTypes` 在任务 7 定义、任务 8/9/10 使用——一致 ✓