|
|
@@ -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 使用——一致 ✓
|