|
|
@@ -0,0 +1,1432 @@
|
|
|
+# 关键人拓展章鱼图替换 实现计划
|
|
|
+
|
|
|
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
|
|
+
|
|
|
+**目标:** 替换现有"帮助记录型章鱼图"(contact_help_logs + OctopusController + OctopusDiagram.vue),上线"关键人拓展型章鱼图":成效记录(会员客户) → 类型bitmask(金额大/频次高/新成交) → 关键人画像(单位/部门/职务/初识场合/如何认识) → 四步法 + 高频词分析 → ≤8 个拓展方向。
|
|
|
+
|
|
|
+**架构:**
|
|
|
+- 后端:Spring Boot 2.7.18 + MyBatis-Plus,3 张新表 + KeyPersonController(新建,不复用 OctopusController),Service 层聚合逻辑,统一 POST + Result<T>。
|
|
|
+- 前端:uni-app Vue 2 Options API,Canvas 2d 绘图(type="2d" + uni.createSelectorQuery + dpr 自适应),替换 action-detail 中的 OctopusDiagram.vue。
|
|
|
+- 数据隔离:纯 `user_id` 租户键,无 child/family 概念。
|
|
|
+
|
|
|
+**技术栈:** Java 8, Spring Boot 2.7.18, MyBatis-Plus, MySQL 8.0, uni-app (Vue 2 Options API), Canvas 2d, uni.createSelectorQuery。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 1:数据库迁移 + schema.sql 同步
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java:10549`(在迁移304后追加)
|
|
|
+- 修改:`cfc-backend/src/main/resources/schema.sql`(在 resource_items 表后追加 3 表)
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写失败的测试(验证迁移不存在)**
|
|
|
+
|
|
|
+```bash
|
|
|
+# 验证当前无这 3 表
|
|
|
+grep -n "octopus_effect_record\|octopus_key_person\|octopus_effect_key_person" cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
|
|
|
+# 预期:无匹配
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:在 DatabaseInitializer 追加迁移 305-307**
|
|
|
+
|
|
|
+```java
|
|
|
+// 迁移305: 创建 octopus_effect_record 表(关键人拓展-成效记录)
|
|
|
+try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS octopus_effect_record (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "user_id BIGINT NOT NULL COMMENT '当前用户(租户键)', " +
|
|
|
+ "member_order_id BIGINT COMMENT '关联 MemberSubscriptionOrder.id(会员订单)', " +
|
|
|
+ "effect_type TINYINT UNSIGNED NOT NULL COMMENT '成效类型 bitmask:1=金额大,2=频次高,4=新成交(支持多选)', " +
|
|
|
+ "effect_amount DECIMAL(10,2) DEFAULT 0 COMMENT '成效金额(元)', " +
|
|
|
+ "effect_desc VARCHAR(500) COMMENT '成效描述', " +
|
|
|
+ "status TINYINT DEFAULT 1 COMMENT '1=有效,0=无效', " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "INDEX idx_oer_user (user_id), " +
|
|
|
+ "INDEX idx_oer_order (member_order_id) " +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关键人拓展-成效记录'");
|
|
|
+ log.info("已创建octopus_effect_record表");
|
|
|
+} catch (Exception e) {
|
|
|
+ log.warn("创建octopus_effect_record表失败: {}", e.getMessage());
|
|
|
+}
|
|
|
+
|
|
|
+// 迁移306: 创建 octopus_key_person 表(关键人画像)
|
|
|
+try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS octopus_key_person (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "user_id BIGINT NOT NULL COMMENT '当前用户(租户键)', " +
|
|
|
+ "name VARCHAR(64) NOT NULL COMMENT '关键人姓名', " +
|
|
|
+ "organization VARCHAR(128) COMMENT '单位/公司', " +
|
|
|
+ "department VARCHAR(64) COMMENT '部门', " +
|
|
|
+ "title VARCHAR(64) COMMENT '职务/职位', " +
|
|
|
+ "first_meet_scene VARCHAR(128) COMMENT '初识场合', " +
|
|
|
+ "know_way VARCHAR(255) COMMENT '如何认识(认识路径)', " +
|
|
|
+ "contact_info VARCHAR(255) COMMENT '联系方式(可选)', " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "INDEX idx_okp_user (user_id), " +
|
|
|
+ "INDEX idx_okp_org (organization) " +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关键人画像'");
|
|
|
+ log.info("已创建octopus_key_person表");
|
|
|
+} catch (Exception e) {
|
|
|
+ log.warn("创建octopus_key_person表失败: {}", e.getMessage());
|
|
|
+}
|
|
|
+
|
|
|
+// 迁移307: 创建 octopus_effect_key_person 表(成效-关键人 多对多)
|
|
|
+try {
|
|
|
+ jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS octopus_effect_key_person (" +
|
|
|
+ "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
|
|
|
+ "effect_id BIGINT NOT NULL COMMENT '关联 octopus_effect_record.id', " +
|
|
|
+ "key_person_id BIGINT NOT NULL COMMENT '关联 octopus_key_person.id', " +
|
|
|
+ "role TINYINT NOT NULL COMMENT '1=引荐,2=决策,3=其他', " +
|
|
|
+ "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
|
+ "INDEX idx_oekp_effect (effect_id), " +
|
|
|
+ "INDEX idx_oekp_person (key_person_id), " +
|
|
|
+ "UNIQUE KEY uk_oekp (effect_id,key_person_id) " +
|
|
|
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成效-关键人关联'");
|
|
|
+ log.info("已创建octopus_effect_key_person表");
|
|
|
+} catch (Exception e) {
|
|
|
+ log.warn("创建octopus_effect_key_person表失败: {}", e.getMessage());
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:同步 schema.sql(在 resource_items 表后追加)**
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 关键人拓展-成效记录
|
|
|
+CREATE TABLE IF NOT EXISTS octopus_effect_record (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ user_id BIGINT NOT NULL COMMENT '当前用户(租户键)',
|
|
|
+ member_order_id BIGINT COMMENT '关联 MemberSubscriptionOrder.id(会员订单)',
|
|
|
+ effect_type TINYINT UNSIGNED NOT NULL COMMENT '成效类型 bitmask:1=金额大,2=频次高,4=新成交(支持多选)',
|
|
|
+ effect_amount DECIMAL(10,2) DEFAULT 0 COMMENT '成效金额(元)',
|
|
|
+ effect_desc VARCHAR(500) COMMENT '成效描述',
|
|
|
+ status TINYINT DEFAULT 1 COMMENT '1=有效,0=无效',
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ INDEX idx_oer_user (user_id),
|
|
|
+ INDEX idx_oer_order (member_order_id)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关键人拓展-成效记录';
|
|
|
+
|
|
|
+-- 关键人画像
|
|
|
+CREATE TABLE IF NOT EXISTS octopus_key_person (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ user_id BIGINT NOT NULL COMMENT '当前用户(租户键)',
|
|
|
+ name VARCHAR(64) NOT NULL COMMENT '关键人姓名',
|
|
|
+ organization VARCHAR(128) COMMENT '单位/公司',
|
|
|
+ department VARCHAR(64) COMMENT '部门',
|
|
|
+ title VARCHAR(64) COMMENT '职务/职位',
|
|
|
+ first_meet_scene VARCHAR(128) COMMENT '初识场合',
|
|
|
+ know_way VARCHAR(255) COMMENT '如何认识(认识路径)',
|
|
|
+ contact_info VARCHAR(255) COMMENT '联系方式(可选)',
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ INDEX idx_okp_user (user_id),
|
|
|
+ INDEX idx_okp_org (organization)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关键人画像';
|
|
|
+
|
|
|
+-- 成效-关键人关联
|
|
|
+CREATE TABLE IF NOT EXISTS octopus_effect_key_person (
|
|
|
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
|
+ effect_id BIGINT NOT NULL COMMENT '关联 octopus_effect_record.id',
|
|
|
+ key_person_id BIGINT NOT NULL COMMENT '关联 octopus_key_person.id',
|
|
|
+ role TINYINT NOT NULL COMMENT '1=引荐,2=决策,3=其他',
|
|
|
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ INDEX idx_oekp_effect (effect_id),
|
|
|
+ INDEX idx_oekp_person (key_person_id),
|
|
|
+ UNIQUE KEY uk_oekp (effect_id,key_person_id)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成效-关键人关联';
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:验证编译**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -DskipTests
|
|
|
+# 预期:BUILD SUCCESS
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5: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 "db: 迁移305-307 关键人拓展 3 表(octopus_effect_record/octopus_key_person/octopus_effect_key_person)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 2:后端 Entity / Mapper
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/OctopusEffectRecord.java`
|
|
|
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/OctopusKeyPerson.java`
|
|
|
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/OctopusEffectKeyPerson.java`
|
|
|
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/OctopusEffectRecordMapper.java`
|
|
|
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/OctopusKeyPersonMapper.java`
|
|
|
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/OctopusEffectKeyPersonMapper.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:OctopusEffectRecord.java**
|
|
|
+
|
|
|
+```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.math.BigDecimal;
|
|
|
+import java.util.Date;
|
|
|
+
|
|
|
+@Data
|
|
|
+@TableName("octopus_effect_record")
|
|
|
+public class OctopusEffectRecord implements Serializable {
|
|
|
+
|
|
|
+ @TableId(type = IdType.AUTO)
|
|
|
+ private Long id;
|
|
|
+
|
|
|
+ private Long userId;
|
|
|
+
|
|
|
+ private Long memberOrderId;
|
|
|
+
|
|
|
+ private Integer effectType; // bitmask
|
|
|
+
|
|
|
+ private BigDecimal effectAmount;
|
|
|
+
|
|
|
+ private String effectDesc;
|
|
|
+
|
|
|
+ private Integer status;
|
|
|
+
|
|
|
+ private Date createdAt;
|
|
|
+
|
|
|
+ // 非持久化:解析后的类型标签列表
|
|
|
+ private transient java.util.List<String> effectTypeList;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:OctopusKeyPerson.java**
|
|
|
+
|
|
|
+```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("octopus_key_person")
|
|
|
+public class OctopusKeyPerson implements Serializable {
|
|
|
+
|
|
|
+ @TableId(type = IdType.AUTO)
|
|
|
+ private Long id;
|
|
|
+
|
|
|
+ private Long userId;
|
|
|
+
|
|
|
+ private String name;
|
|
|
+
|
|
|
+ private String organization;
|
|
|
+
|
|
|
+ private String department;
|
|
|
+
|
|
|
+ private String title;
|
|
|
+
|
|
|
+ private String firstMeetScene;
|
|
|
+
|
|
|
+ private String knowWay;
|
|
|
+
|
|
|
+ private String contactInfo;
|
|
|
+
|
|
|
+ private Date createdAt;
|
|
|
+
|
|
|
+ // 非持久化:角色名
|
|
|
+ private transient String roleName;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:OctopusEffectKeyPerson.java**
|
|
|
+
|
|
|
+```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("octopus_effect_key_person")
|
|
|
+public class OctopusEffectKeyPerson implements Serializable {
|
|
|
+
|
|
|
+ @TableId(type = IdType.AUTO)
|
|
|
+ private Long id;
|
|
|
+
|
|
|
+ private Long effectId;
|
|
|
+
|
|
|
+ private Long keyPersonId;
|
|
|
+
|
|
|
+ private Integer role; // 1=引荐,2=决策,3=其他
|
|
|
+
|
|
|
+ private Date createdAt;
|
|
|
+
|
|
|
+ // 非持久化
|
|
|
+ private transient String roleName;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:3 个 Mapper 接口(仅继承 BaseMapper)**
|
|
|
+
|
|
|
+```java
|
|
|
+// OctopusEffectRecordMapper.java
|
|
|
+package com.etotem.cfc.mapper;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
+import com.etotem.cfc.entity.OctopusEffectRecord;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface OctopusEffectRecordMapper extends BaseMapper<OctopusEffectRecord> {
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```java
|
|
|
+// OctopusKeyPersonMapper.java
|
|
|
+package com.etotem.cfc.mapper;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
+import com.etotem.cfc.entity.OctopusKeyPerson;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface OctopusKeyPersonMapper extends BaseMapper<OctopusKeyPerson> {
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```java
|
|
|
+// OctopusEffectKeyPersonMapper.java
|
|
|
+package com.etotem.cfc.mapper;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
+import com.etotem.cfc.entity.OctopusEffectKeyPerson;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface OctopusEffectKeyPersonMapper extends BaseMapper<OctopusEffectKeyPerson> {
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:验证编译 + Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -DskipTests
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/Octopus*.java cfc-backend/src/main/java/com/etotem/cfc/mapper/Octopus*Mapper.java
|
|
|
+git commit -m "feat: 新增 3 实体 + 3 Mapper(octopus_effect_record/octopus_key_person/octopus_effect_key_person)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 3:KeyPersonService 核心业务逻辑
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/KeyPersonService.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:编写失败的测试(验证类不存在)**
|
|
|
+
|
|
|
+```bash
|
|
|
+ls cfc-backend/src/main/java/com/etotem/cfc/service/KeyPersonService.java
|
|
|
+# 预期:No such file
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:实现 KeyPersonService.java(参考 ResourceService 模式)**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.entity.MemberSubscriptionOrder;
|
|
|
+import com.etotem.cfc.entity.OctopusEffectKeyPerson;
|
|
|
+import com.etotem.cfc.entity.OctopusEffectRecord;
|
|
|
+import com.etotem.cfc.entity.OctopusKeyPerson;
|
|
|
+import com.etotem.cfc.mapper.MemberSubscriptionOrderMapper;
|
|
|
+import com.etotem.cfc.mapper.OctopusEffectKeyPersonMapper;
|
|
|
+import com.etotem.cfc.mapper.OctopusEffectRecordMapper;
|
|
|
+import com.etotem.cfc.mapper.OctopusKeyPersonMapper;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.util.*;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class KeyPersonService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private OctopusEffectRecordMapper effectRecordMapper;
|
|
|
+ @Resource
|
|
|
+ private OctopusKeyPersonMapper keyPersonMapper;
|
|
|
+ @Resource
|
|
|
+ private OctopusEffectKeyPersonMapper ekpMapper;
|
|
|
+ @Resource
|
|
|
+ private MemberSubscriptionOrderMapper memberOrderMapper;
|
|
|
+
|
|
|
+ // ========== 1. 盘点项目 ==========
|
|
|
+
|
|
|
+ public Result<Map<String, Object>> listEffects(Long userId, Integer effectType, Integer page, Integer pageSize) {
|
|
|
+ Page<OctopusEffectRecord> p = new Page<>(page, pageSize);
|
|
|
+ LambdaQueryWrapper<OctopusEffectRecord> q = new LambdaQueryWrapper<OctopusEffectRecord>()
|
|
|
+ .eq(OctopusEffectRecord::getUserId, userId);
|
|
|
+ if (effectType != null && effectType > 0) {
|
|
|
+ q.apply("effect_type & {0}", effectType);
|
|
|
+ }
|
|
|
+ q.orderByDesc(OctopusEffectRecord::getCreatedAt);
|
|
|
+ effectRecordMapper.selectPage(p, q);
|
|
|
+
|
|
|
+ List<Map<String, Object>> list = p.getRecords().stream().map(this::toEffectVO).collect(Collectors.toList());
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
+ data.put("list", list);
|
|
|
+ data.put("total", p.getTotal());
|
|
|
+ data.put("page", page);
|
|
|
+ data.put("pageSize", pageSize);
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> toEffectVO(OctopusEffectRecord r) {
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
+ m.put("id", r.getId());
|
|
|
+ m.put("memberOrderId", r.getMemberOrderId());
|
|
|
+ m.put("effectType", r.getEffectType());
|
|
|
+ m.put("effectTypeList", parseEffectType(r.getEffectType()));
|
|
|
+ m.put("effectAmount", r.getEffectAmount());
|
|
|
+ m.put("effectDesc", r.getEffectDesc());
|
|
|
+ m.put("status", r.getStatus());
|
|
|
+ m.put("createdAt", r.getCreatedAt() != null ? r.getCreatedAt().toString() : "");
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<String> parseEffectType(Integer type) {
|
|
|
+ List<String> list = new ArrayList<>();
|
|
|
+ if (type == null) return list;
|
|
|
+ if ((type & 1) != 0) list.add("金额大");
|
|
|
+ if ((type & 2) != 0) list.add("频次高");
|
|
|
+ if ((type & 4) != 0) list.add("新成交");
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Result<OctopusEffectRecord> addEffect(Long userId, Long memberOrderId, Integer effectType,
|
|
|
+ BigDecimal effectAmount, String effectDesc) {
|
|
|
+ // 可选:校验 memberOrderId 归属
|
|
|
+ if (memberOrderId != null) {
|
|
|
+ MemberSubscriptionOrder order = memberOrderMapper.selectById(memberOrderId);
|
|
|
+ if (order == null || !order.getUserId().equals(userId)) {
|
|
|
+ return Result.error("会员订单不存在");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ OctopusEffectRecord r = new OctopusEffectRecord();
|
|
|
+ r.setUserId(userId);
|
|
|
+ r.setMemberOrderId(memberOrderId);
|
|
|
+ r.setEffectType(effectType != null ? effectType : 0);
|
|
|
+ r.setEffectAmount(effectAmount != null ? effectAmount : BigDecimal.ZERO);
|
|
|
+ r.setEffectDesc(effectDesc != null ? effectDesc.trim() : null);
|
|
|
+ r.setStatus(1);
|
|
|
+ r.setCreatedAt(new Date());
|
|
|
+ effectRecordMapper.insert(r);
|
|
|
+ return Result.success(r);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Result<String> deleteEffect(Long userId, Long effectId) {
|
|
|
+ OctopusEffectRecord r = effectRecordMapper.selectById(effectId);
|
|
|
+ if (r == null || !r.getUserId().equals(userId)) {
|
|
|
+ return Result.error("成效记录不存在");
|
|
|
+ }
|
|
|
+ // 级联删除关联关系
|
|
|
+ ekpMapper.delete(new LambdaQueryWrapper<OctopusEffectKeyPerson>().eq(OctopusEffectKeyPerson::getEffectId, effectId));
|
|
|
+ effectRecordMapper.deleteById(effectId);
|
|
|
+ return Result.success(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 2. 找关键人 ==========
|
|
|
+
|
|
|
+ public Result<OctopusKeyPerson> addKeyPerson(Long userId, String name, String organization,
|
|
|
+ String department, String title,
|
|
|
+ String firstMeetScene, String knowWay,
|
|
|
+ String contactInfo) {
|
|
|
+ if (name == null || name.trim().isEmpty()) {
|
|
|
+ return Result.error("关键人姓名不能为空");
|
|
|
+ }
|
|
|
+ OctopusKeyPerson kp = new OctopusKeyPerson();
|
|
|
+ kp.setUserId(userId);
|
|
|
+ kp.setName(name.trim());
|
|
|
+ kp.setOrganization(organization != null ? organization.trim() : null);
|
|
|
+ kp.setDepartment(department != null ? department.trim() : null);
|
|
|
+ kp.setTitle(title != null ? title.trim() : null);
|
|
|
+ kp.setFirstMeetScene(firstMeetScene != null ? firstMeetScene.trim() : null);
|
|
|
+ kp.setKnowWay(knowWay != null ? knowWay.trim() : null);
|
|
|
+ kp.setContactInfo(contactInfo != null ? contactInfo.trim() : null);
|
|
|
+ kp.setCreatedAt(new Date());
|
|
|
+ keyPersonMapper.insert(kp);
|
|
|
+ return Result.success(kp);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Result<OctopusKeyPerson> updateKeyPerson(Long userId, Long id, Map<String, Object> params) {
|
|
|
+ OctopusKeyPerson kp = keyPersonMapper.selectById(id);
|
|
|
+ if (kp == null || !kp.getUserId().equals(userId)) {
|
|
|
+ return Result.error("关键人不存在");
|
|
|
+ }
|
|
|
+ if (params.containsKey("name") && params.get("name") != null) kp.setName(params.get("name").toString().trim());
|
|
|
+ if (params.containsKey("organization")) kp.setOrganization(params.get("organization") != null ? params.get("organization").toString().trim() : null);
|
|
|
+ if (params.containsKey("department")) kp.setDepartment(params.get("department") != null ? params.get("department").toString().trim() : null);
|
|
|
+ if (params.containsKey("title")) kp.setTitle(params.get("title") != null ? params.get("title").toString().trim() : null);
|
|
|
+ if (params.containsKey("firstMeetScene")) kp.setFirstMeetScene(params.get("firstMeetScene") != null ? params.get("firstMeetScene").toString().trim() : null);
|
|
|
+ if (params.containsKey("knowWay")) kp.setKnowWay(params.get("knowWay") != null ? params.get("knowWay").toString().trim() : null);
|
|
|
+ if (params.containsKey("contactInfo")) kp.setContactInfo(params.get("contactInfo") != null ? params.get("contactInfo").toString().trim() : null);
|
|
|
+ keyPersonMapper.updateById(kp);
|
|
|
+ return Result.success(kp);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Result<String> deleteKeyPerson(Long userId, Long id) {
|
|
|
+ OctopusKeyPerson kp = keyPersonMapper.selectById(id);
|
|
|
+ if (kp == null || !kp.getUserId().equals(userId)) {
|
|
|
+ return Result.error("关键人不存在");
|
|
|
+ }
|
|
|
+ // 级联删除关联关系
|
|
|
+ ekpMapper.delete(new LambdaQueryWrapper<OctopusEffectKeyPerson>().eq(OctopusEffectKeyPerson::getKeyPersonId, id));
|
|
|
+ keyPersonMapper.deleteById(id);
|
|
|
+ return Result.success(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ public Result<List<Map<String, Object>>> listKeyPersonsByEffect(Long userId, Long effectId) {
|
|
|
+ // 校验成效归属
|
|
|
+ OctopusEffectRecord eff = effectRecordMapper.selectById(effectId);
|
|
|
+ if (eff == null || !eff.getUserId().equals(userId)) {
|
|
|
+ return Result.error("成效记录不存在");
|
|
|
+ }
|
|
|
+ List<OctopusEffectKeyPerson> rels = ekpMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<OctopusEffectKeyPerson>().eq(OctopusEffectKeyPerson::getEffectId, effectId)
|
|
|
+ );
|
|
|
+ if (rels.isEmpty()) return Result.success(new ArrayList<>());
|
|
|
+
|
|
|
+ List<Long> kpIds = rels.stream().map(OctopusEffectKeyPerson::getKeyPersonId).collect(Collectors.toList());
|
|
|
+ Map<Long, OctopusKeyPerson> kpMap = keyPersonMapper.selectBatchIds(kpIds).stream()
|
|
|
+ .collect(Collectors.toMap(OctopusKeyPerson::getId, k -> k));
|
|
|
+ Map<Long, Integer> roleMap = rels.stream().collect(Collectors.toMap(OctopusEffectKeyPerson::getKeyPersonId, OctopusEffectKeyPerson::getRole));
|
|
|
+
|
|
|
+ List<Map<String, Object>> list = new ArrayList<>();
|
|
|
+ for (Long kpId : kpIds) {
|
|
|
+ OctopusKeyPerson kp = kpMap.get(kpId);
|
|
|
+ if (kp == null) continue;
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
+ m.put("id", kp.getId());
|
|
|
+ m.put("name", kp.getName());
|
|
|
+ m.put("organization", kp.getOrganization());
|
|
|
+ m.put("department", kp.getDepartment());
|
|
|
+ m.put("title", kp.getTitle());
|
|
|
+ m.put("firstMeetScene", kp.getFirstMeetScene());
|
|
|
+ m.put("knowWay", kp.getKnowWay());
|
|
|
+ m.put("contactInfo", kp.getContactInfo());
|
|
|
+ m.put("role", roleMap.get(kpId));
|
|
|
+ m.put("roleName", roleName(roleMap.get(kpId)));
|
|
|
+ m.put("createdAt", kp.getCreatedAt() != null ? kp.getCreatedAt().toString() : "");
|
|
|
+ list.add(m);
|
|
|
+ }
|
|
|
+ return Result.success(list);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 3. 分析关键人 ==========
|
|
|
+
|
|
|
+ public Result<Map<String, Object>> analyzeKeywords(Long userId) {
|
|
|
+ List<OctopusKeyPerson> kps = keyPersonMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<OctopusKeyPerson>().eq(OctopusKeyPerson::getUserId, userId)
|
|
|
+ );
|
|
|
+ // 统计各维度词频
|
|
|
+ Map<String, Integer> orgFreq = new HashMap<>();
|
|
|
+ Map<String, Integer> deptFreq = new HashMap<>();
|
|
|
+ Map<String, Integer> titleFreq = new HashMap<>();
|
|
|
+ Map<String, Integer> sceneFreq = new HashMap<>();
|
|
|
+ Map<String, Integer> wayFreq = new HashMap<>();
|
|
|
+
|
|
|
+ for (OctopusKeyPerson kp : kps) {
|
|
|
+ inc(orgFreq, kp.getOrganization());
|
|
|
+ inc(deptFreq, kp.getDepartment());
|
|
|
+ inc(titleFreq, kp.getTitle());
|
|
|
+ inc(sceneFreq, kp.getFirstMeetScene());
|
|
|
+ inc(wayFreq, kp.getKnowWay());
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
+ data.put("organization", topN(orgFreq, 20));
|
|
|
+ data.put("department", topN(deptFreq, 20));
|
|
|
+ data.put("title", topN(titleFreq, 20));
|
|
|
+ data.put("firstMeetScene", topN(sceneFreq, 20));
|
|
|
+ data.put("knowWay", topN(wayFreq, 20));
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void inc(Map<String, Integer> map, String val) {
|
|
|
+ if (val == null || val.trim().isEmpty()) return;
|
|
|
+ map.put(val, map.getOrDefault(val, 0) + 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Map<String, Object>> topN(Map<String, Integer> map, int n) {
|
|
|
+ return map.entrySet().stream()
|
|
|
+ .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
|
|
|
+ .limit(n)
|
|
|
+ .map(e -> {
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
+ m.put("keyword", e.getKey());
|
|
|
+ m.put("count", e.getValue());
|
|
|
+ return m;
|
|
|
+ }).collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ public Result<Map<String, Object>> analysisSummary(Long userId) {
|
|
|
+ List<OctopusKeyPerson> kps = keyPersonMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<OctopusKeyPerson>().eq(OctopusKeyPerson::getUserId, userId)
|
|
|
+ );
|
|
|
+ // 聚合:按 organization/title/department/firstMeetScene/knowWay 维度
|
|
|
+ List<Map<String, Object>> directions = new ArrayList<>();
|
|
|
+
|
|
|
+ addDirections(directions, kps, "organization", "单位", 8);
|
|
|
+ addDirections(directions, kps, "title", "职务", 8);
|
|
|
+ addDirections(directions, kps, "department", "部门", 8);
|
|
|
+ addDirections(directions, kps, "firstMeetScene", "初识场合", 8);
|
|
|
+ addDirections(directions, kps, "knowWay", "认识路径", 8);
|
|
|
+
|
|
|
+ // 全局排序取 Top 8
|
|
|
+ directions.sort((a, b) -> Integer.compare((Integer) b.get("count"), (Integer) a.get("count")));
|
|
|
+ if (directions.size() > 8) directions = directions.subList(0, 8);
|
|
|
+
|
|
|
+ Map<String, Object> data = new LinkedHashMap<>();
|
|
|
+ data.put("directions", directions);
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addDirections(List<Map<String, Object>> directions, List<OctopusKeyPerson> kps,
|
|
|
+ String field, String dimName, int maxPerDim) {
|
|
|
+ Map<String, Integer> freq = new HashMap<>();
|
|
|
+ for (OctopusKeyPerson kp : kps) {
|
|
|
+ String val = getField(kp, field);
|
|
|
+ if (val != null && !val.trim().isEmpty()) {
|
|
|
+ freq.put(val, freq.getOrDefault(val, 0) + 1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ freq.entrySet().stream()
|
|
|
+ .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
|
|
|
+ .limit(maxPerDim)
|
|
|
+ .forEach(e -> {
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
+ m.put("dimension", field);
|
|
|
+ m.put("keyword", e.getKey());
|
|
|
+ m.put("count", e.getValue());
|
|
|
+ m.put("reason", genReason(field, e.getKey()));
|
|
|
+ directions.add(m);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getField(OctopusKeyPerson kp, String field) {
|
|
|
+ switch (field) {
|
|
|
+ case "organization": return kp.getOrganization();
|
|
|
+ case "title": return kp.getTitle();
|
|
|
+ case "department": return kp.getDepartment();
|
|
|
+ case "firstMeetScene": return kp.getFirstMeetScene();
|
|
|
+ case "knowWay": return kp.getKnowWay();
|
|
|
+ default: return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String genReason(String field, String keyword) {
|
|
|
+ switch (field) {
|
|
|
+ case "organization": return "该单位关键人高度聚集";
|
|
|
+ case "title": return "该职务人脉密集";
|
|
|
+ case "department": return "该部门关键人多";
|
|
|
+ case "firstMeetScene": return "该场合易识别关键人";
|
|
|
+ case "knowWay": return "该认识路径高频";
|
|
|
+ default: return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 4. 拓展关键人 ==========
|
|
|
+
|
|
|
+ public Result<List<Map<String, Object>>> expansionSuggest(Long userId) {
|
|
|
+ // 复用 analysisSummary 的方向,为每个方向生成预填模板
|
|
|
+ Result<Map<String, Object>> summaryRes = analysisSummary(userId);
|
|
|
+ if (summaryRes.getCode() != 200) return Result.success(new ArrayList<>());
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ List<Map<String, Object>> dirs = (List<Map<String, Object>>) summaryRes.getData().get("directions");
|
|
|
+ List<Map<String, Object>> suggests = new ArrayList<>();
|
|
|
+ for (Map<String, Object> d : dirs) {
|
|
|
+ Map<String, Object> tpl = new LinkedHashMap<>();
|
|
|
+ tpl.put("dimension", d.get("dimension"));
|
|
|
+ tpl.put("keyword", d.get("keyword"));
|
|
|
+ tpl.put("prefill", buildPrefill((String) d.get("dimension"), (String) d.get("keyword")));
|
|
|
+ suggests.add(tpl);
|
|
|
+ }
|
|
|
+ return Result.success(suggests);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> buildPrefill(String dim, String keyword) {
|
|
|
+ Map<String, Object> p = new LinkedHashMap<>();
|
|
|
+ // 只预填对应维度字段,其余留空供用户补充
|
|
|
+ switch (dim) {
|
|
|
+ case "organization":
|
|
|
+ p.put("organization", keyword);
|
|
|
+ break;
|
|
|
+ case "title":
|
|
|
+ p.put("title", keyword);
|
|
|
+ break;
|
|
|
+ case "department":
|
|
|
+ p.put("department", keyword);
|
|
|
+ break;
|
|
|
+ case "firstMeetScene":
|
|
|
+ p.put("firstMeetScene", keyword);
|
|
|
+ break;
|
|
|
+ case "knowWay":
|
|
|
+ p.put("knowWay", keyword);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String roleName(Integer role) {
|
|
|
+ if (role == null) return "";
|
|
|
+ switch (role) {
|
|
|
+ case 1: return "引荐";
|
|
|
+ case 2: return "决策";
|
|
|
+ case 3: return "其他";
|
|
|
+ default: return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:验证编译 + Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -DskipTests
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/KeyPersonService.java
|
|
|
+git commit -m "feat: KeyPersonService 核心业务逻辑(四步法全接口实现)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 4:KeyPersonController(新建,不复用 OctopusController)
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/KeyPersonController.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:实现 Controller(完全遵循 PearlController 模式)**
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.controller;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.service.KeyPersonService;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+@RestController
|
|
|
+@RequestMapping("/api/octopus")
|
|
|
+public class KeyPersonController {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private KeyPersonService keyPersonService;
|
|
|
+
|
|
|
+ // ===== 1. 盘点项目 =====
|
|
|
+ @PostMapping("/effect/list")
|
|
|
+ public Result<Map<String, Object>> listEffects(@RequestBody(required = false) Map<String, Object> params,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ Integer effectType = params != null && params.get("effectType") != null
|
|
|
+ ? Integer.valueOf(params.get("effectType").toString()) : null;
|
|
|
+ Integer page = params != null && params.get("page") != null
|
|
|
+ ? Integer.valueOf(params.get("page").toString()) : 1;
|
|
|
+ Integer pageSize = params != null && params.get("pageSize") != null
|
|
|
+ ? Integer.valueOf(params.get("pageSize").toString()) : 20;
|
|
|
+ return keyPersonService.listEffects(userId, effectType, page, pageSize);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/effect/add")
|
|
|
+ public Result<com.etotem.cfc.entity.OctopusEffectRecord> addEffect(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ Long memberOrderId = params.get("memberOrderId") != null
|
|
|
+ ? Long.valueOf(params.get("memberOrderId").toString()) : null;
|
|
|
+ Integer effectType = params.get("effectType") != null
|
|
|
+ ? Integer.valueOf(params.get("effectType").toString()) : 0;
|
|
|
+ BigDecimal effectAmount = params.get("effectAmount") != null
|
|
|
+ ? new BigDecimal(params.get("effectAmount").toString()) : BigDecimal.ZERO;
|
|
|
+ String effectDesc = (String) params.get("effectDesc");
|
|
|
+ return keyPersonService.addEffect(userId, memberOrderId, effectType, effectAmount, effectDesc);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/effect/delete")
|
|
|
+ public Result<String> deleteEffect(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ Long effectId = params.get("effectId") != null
|
|
|
+ ? Long.valueOf(params.get("effectId").toString()) : null;
|
|
|
+ return keyPersonService.deleteEffect(userId, effectId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 2. 找关键人 =====
|
|
|
+ @PostMapping("/key-person/add")
|
|
|
+ public Result<com.etotem.cfc.entity.OctopusKeyPerson> addKeyPerson(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ String name = (String) params.get("name");
|
|
|
+ String organization = (String) params.get("organization");
|
|
|
+ String department = (String) params.get("department");
|
|
|
+ String title = (String) params.get("title");
|
|
|
+ String firstMeetScene = (String) params.get("firstMeetScene");
|
|
|
+ String knowWay = (String) params.get("knowWay");
|
|
|
+ String contactInfo = (String) params.get("contactInfo");
|
|
|
+ return keyPersonService.addKeyPerson(userId, name, organization, department, title,
|
|
|
+ firstMeetScene, knowWay, contactInfo);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/key-person/update")
|
|
|
+ public Result<com.etotem.cfc.entity.OctopusKeyPerson> updateKeyPerson(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
|
|
|
+ return keyPersonService.updateKeyPerson(userId, id, params);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/key-person/delete")
|
|
|
+ public Result<String> deleteKeyPerson(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
|
|
|
+ return keyPersonService.deleteKeyPerson(userId, id);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/effect/key-persons")
|
|
|
+ public Result<List<Map<String, Object>>> effectKeyPersons(@RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ Long effectId = params.get("effectId") != null ? Long.valueOf(params.get("effectId").toString()) : null;
|
|
|
+ return keyPersonService.listKeyPersonsByEffect(userId, effectId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 3. 分析关键人 =====
|
|
|
+ @PostMapping("/analysis/keywords")
|
|
|
+ public Result<Map<String, Object>> analysisKeywords(@RequestAttribute("userId") Long userId) {
|
|
|
+ return keyPersonService.analyzeKeywords(userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ @PostMapping("/analysis/summary")
|
|
|
+ public Result<Map<String, Object>> analysisSummary(@RequestAttribute("userId") Long userId) {
|
|
|
+ return keyPersonService.analysisSummary(userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 4. 拓展关键人 =====
|
|
|
+ @PostMapping("/expansion/suggest")
|
|
|
+ public Result<List<Map<String, Object>>> expansionSuggest(@RequestAttribute("userId") Long userId) {
|
|
|
+ return keyPersonService.expansionSuggest(userId);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:验证编译 + Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -DskipTests
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/KeyPersonController.java
|
|
|
+git commit -m "feat: KeyPersonController 10 个接口(/api/octopus/effect/*, /key-person/*, /analysis/*, /expansion/suggest)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 5:废弃旧 OctopusController + 清理
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/OctopusController.java`
|
|
|
+- 删除:`cfc-frontend/components/OctopusDiagram.vue`
|
|
|
+- 删除:`cfc-frontend/pages/action-detail/octopus-add-help.vue`
|
|
|
+- 删除:`cfc-frontend/pages/action-detail/octopus-records.vue`
|
|
|
+- 修改:`cfc-frontend/pages.json`(移除 octopus-add-help, octopus-records 路由)
|
|
|
+- 修改:`cfc-frontend/utils/api.js`(移除旧 4 个 octopus 封装)
|
|
|
+- 修改:`docs/superpowers/api/API_REFERENCE.md`(标记旧接口废弃)
|
|
|
+
|
|
|
+- [ ] **步骤 1:OctopusController 标记 410**
|
|
|
+
|
|
|
+```java
|
|
|
+// 修改每个方法第一行返回 410
|
|
|
+@PostMapping("/tentacles")
|
|
|
+public Result<List<Map<String, Object>>> tentacles(@RequestAttribute("userId") Long userId) {
|
|
|
+ return Result.error(410, "该接口已废弃,请使用 /api/octopus/effect/list 等新接口");
|
|
|
+}
|
|
|
+@PostMapping("/add")
|
|
|
+public Result<ContactHelpLogDTO> addHelpLog(...) { return Result.error(410, "..."); }
|
|
|
+@PostMapping("/delete")
|
|
|
+public Result<String> deleteHelpLog(...) { return Result.error(410, "..."); }
|
|
|
+@PostMapping("/records")
|
|
|
+public Result<Map<String, Object>> records(...) { return Result.error(410, "..."); }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:前端删除旧文件 + 更新 pages.json + api.js**
|
|
|
+
|
|
|
+```bash
|
|
|
+# 删除文件
|
|
|
+rm cfc-frontend/components/OctopusDiagram.vue
|
|
|
+rm cfc-frontend/pages/action-detail/octopus-add-help.vue
|
|
|
+rm cfc-frontend/pages/action-detail/octopus-records.vue
|
|
|
+
|
|
|
+# pages.json 移除 "octopus-add-help", "octopus-records" 两项
|
|
|
+# api.js 删除 getOctopusTentacles/addOctopusHelpLog/deleteOctopusHelpLog/getOctopusRecords 4 行
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:API_REFERENCE.md 标记旧接口废弃**
|
|
|
+
|
|
|
+在 "4.37 情绪识别" 后或废弃列表中加:
|
|
|
+```
|
|
|
+### 4.xx 章鱼图(旧,已废弃 410)
|
|
|
+| 路径 | 说明 | 替代 |
|
|
|
+|------|------|------|
|
|
|
+| POST /api/octopus/tentacles | 触手榜 | /api/octopus/effect/list |
|
|
|
+| POST /api/octopus/add | 新增帮助记录 | /api/octopus/effect/add |
|
|
|
+| POST /api/octopus/delete | 删除帮助记录 | /api/octopus/effect/delete |
|
|
|
+| POST /api/octopus/records | 某联系人帮助明细 | /api/octopus/effect/key-persons |
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:验证编译 + Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -DskipTests
|
|
|
+git add -A
|
|
|
+git commit -m "refactor: 废弃旧章鱼图接口/组件/页面/文档(410 Gone),清理 3 旧文件"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 6:前端 api.js 新增 10 个封装
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/utils/api.js`(在 getAbilityMap 后追加)
|
|
|
+
|
|
|
+- [ ] **步骤 1:追加 10 个函数**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// ===== 关键人拓展章鱼图 =====
|
|
|
+export const getOctopusEffects = (data) => request('/api/octopus/effect/list', 'POST', data)
|
|
|
+export const addOctopusEffect = (data) => request('/api/octopus/effect/add', 'POST', data)
|
|
|
+export const deleteOctopusEffect = (data) => request('/api/octopus/effect/delete', 'POST', data)
|
|
|
+
|
|
|
+export const addOctopusKeyPerson = (data) => request('/api/octopus/key-person/add', 'POST', data)
|
|
|
+export const updateOctopusKeyPerson = (data) => request('/api/octopus/key-person/update', 'POST', data)
|
|
|
+export const deleteOctopusKeyPerson = (data) => request('/api/octopus/key-person/delete', 'POST', data)
|
|
|
+export const getOctopusEffectKeyPersons = (data) => request('/api/octopus/effect/key-persons', 'POST', data)
|
|
|
+
|
|
|
+export const getOctopusAnalysisKeywords = (data) => request('/api/octopus/analysis/keywords', 'POST', data)
|
|
|
+export const getOctopusAnalysisSummary = (data) => request('/api/octopus/analysis/summary', 'POST', data)
|
|
|
+export const getOctopusExpansionSuggest = (data) => request('/api/octopus/expansion/suggest', 'POST', data)
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/utils/api.js
|
|
|
+git commit -m "feat: api.js 新增 10 个关键人拓展接口封装"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 7:前端页面——成效录入页
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`cfc-frontend/pages/action-detail/octopus-add-effect.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:参考 pearl-add-resource.vue 风格实现**
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <view class="container">
|
|
|
+ <view class="page-header">
|
|
|
+ <text class="page-title">记录成效</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 关联会员订单(可选) -->
|
|
|
+ <view class="form-section">
|
|
|
+ <text class="section-label">关联会员订单(选填)</text>
|
|
|
+ <view class="order-select" v-if="orders.length > 0">
|
|
|
+ <view class="order-item" v-for="o in orders" :key="o.id"
|
|
|
+ :class="{selected: selectedOrderId === o.id}"
|
|
|
+ @click="selectedOrderId = o.id">
|
|
|
+ <text class="order-no">{{ o.orderNo }}</text>
|
|
|
+ <text class="order-amount">¥{{ (o.actualPrice/100).toFixed(2) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <text class="empty-hint" v-else>暂无会员订单,可不关联</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 成效类型(多选) -->
|
|
|
+ <view class="form-section">
|
|
|
+ <text class="section-label">成效类型 <text class="required-mark">*</text></text>
|
|
|
+ <view class="type-grid">
|
|
|
+ <view class="type-item" v-for="(name, key) in effectTypes" :key="key"
|
|
|
+ :class="{selected: effectTypeList.includes(Number(key))}"
|
|
|
+ @click="toggleType(Number(key))">
|
|
|
+ <text class="type-icon">{{ typeIcons[key] }}</text>
|
|
|
+ <text class="type-name">{{ name }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 成效金额 -->
|
|
|
+ <view class="form-section">
|
|
|
+ <text class="section-label">成效金额(元)</text>
|
|
|
+ <input class="amount-input" v-model="effectAmount" type="digit" placeholder="如 2500.00" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 成效描述 -->
|
|
|
+ <view class="form-section">
|
|
|
+ <text class="section-label">成效描述(选填)</text>
|
|
|
+ <textarea class="desc-input" v-model="effectDesc" placeholder="简述成效内容" maxlength="200"></textarea>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="submit-bar">
|
|
|
+ <button class="btn-submit" @click="onSubmit" :disabled="!canSubmit">保存成效</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getMemberOrders, addOctopusEffect } from '../../utils/api.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ orders: [],
|
|
|
+ selectedOrderId: null,
|
|
|
+ effectTypeList: [],
|
|
|
+ effectTypes: {1: '金额大', 2: '频次高', 4: '新成交'},
|
|
|
+ typeIcons: {1: '💰', 2: '🔁', 4: '✨'},
|
|
|
+ effectAmount: '',
|
|
|
+ effectDesc: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ canSubmit() {
|
|
|
+ return this.effectTypeList.length > 0 && this.effectAmount.trim()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad() {
|
|
|
+ this.loadOrders()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async loadOrders() {
|
|
|
+ try {
|
|
|
+ var res = await getMemberOrders({ page: 1, size: 100 })
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ this.orders = res.data.list || (Array.isArray(res.data) ? res.data : [])
|
|
|
+ }
|
|
|
+ } catch (e) { /* 静默 */ }
|
|
|
+ },
|
|
|
+ toggleType(val) {
|
|
|
+ var idx = this.effectTypeList.indexOf(val)
|
|
|
+ if (idx > -1) this.effectTypeList.splice(idx, 1)
|
|
|
+ else this.effectTypeList.push(val)
|
|
|
+ },
|
|
|
+ async onSubmit() {
|
|
|
+ if (!this.canSubmit) {
|
|
|
+ uni.showToast({ title: '请选择类型并填写金额', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var type = 0
|
|
|
+ this.effectTypeList.forEach(v => { type |= v })
|
|
|
+ var data = { effectType: type, effectAmount: this.effectAmount, effectDesc: this.effectDesc.trim() }
|
|
|
+ if (this.selectedOrderId) data.memberOrderId = this.selectedOrderId
|
|
|
+ try {
|
|
|
+ var res = await addOctopusEffect(data)
|
|
|
+ if (res.code === 200) {
|
|
|
+ uni.showToast({ title: '保存成功', icon: 'success' })
|
|
|
+ setTimeout(() => uni.navigateBack(), 1500)
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '保存失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (e) { uni.showToast({ title: '保存失败', icon: 'none' }) }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+<!-- 样式复用 pearl-add-resource.vue 的 .form-section/.type-grid/.submit-bar 等 -->
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:注册 pages.json**
|
|
|
+
|
|
|
+```json
|
|
|
+// pages.json action-detail 分包中,在 pearl-add-resource 后加:
|
|
|
+{
|
|
|
+ "path": "octopus-add-effect",
|
|
|
+ "style": { "navigationBarTitleText": "记录成效" }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages/action-detail/octopus-add-effect.vue cfc-frontend/pages.json
|
|
|
+git commit -m "feat: octopus-add-effect 成效录入页 + pages.json 注册"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 8:前端页面——关键人录入页
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`cfc-frontend/pages/action-detail/octopus-add-key-person.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:实现(支持预填 prefill 参数)**
|
|
|
+
|
|
|
+```vue
|
|
|
+<template>
|
|
|
+ <view class="container">
|
|
|
+ <view class="page-header"><text class="page-title">添加关键人</text></view>
|
|
|
+
|
|
|
+ <view class="form-section">
|
|
|
+ <text class="section-label">姓名 <text class="required-mark">*</text></text>
|
|
|
+ <input class="name-input" v-model="name" placeholder="关键人姓名" maxlength="30" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-section">
|
|
|
+ <text class="section-label">单位/公司</text>
|
|
|
+ <input class="name-input" v-model="organization" placeholder="如:某小学家委会、某科技公司" maxlength="60" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-section">
|
|
|
+ <text class="section-label">部门</text>
|
|
|
+ <input class="name-input" v-model="department" placeholder="如:市场部、教务处" maxlength="30" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-section">
|
|
|
+ <text class="section-label">职务/职位</text>
|
|
|
+ <input class="name-input" v-model="title" placeholder="如:家委会主任、市场总监" maxlength="30" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-section">
|
|
|
+ <text class="section-label">初识场合</text>
|
|
|
+ <input class="name-input" v-model="firstMeetScene" placeholder="如:家长会、行业峰会、朋友介绍" maxlength="50" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-section">
|
|
|
+ <text class="section-label">如何认识</text>
|
|
|
+ <textarea class="desc-input" v-model="knowWay" placeholder="如:孩子班主任介绍认识、同事推荐" maxlength="200"></textarea>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="form-section">
|
|
|
+ <text class="section-label">联系方式(选填)</text>
|
|
|
+ <input class="name-input" v-model="contactInfo" placeholder="电话/微信/邮箱" maxlength="50" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="submit-bar">
|
|
|
+ <button class="btn-submit" @click="onSubmit" :disabled="!name.trim()">保存关键人</button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { addOctopusKeyPerson } from '../../utils/api.js'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ name: '', organization: '', department: '', title: '',
|
|
|
+ firstMeetScene: '', knowWay: '', contactInfo: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad(options) {
|
|
|
+ if (options.prefill) {
|
|
|
+ try {
|
|
|
+ var prefill = JSON.parse(decodeURIComponent(options.prefill))
|
|
|
+ Object.assign(this.$data, prefill)
|
|
|
+ } catch (e) { /* 忽略 */ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async onSubmit() {
|
|
|
+ if (!this.name.trim()) { uni.showToast({ title: '请填写姓名', icon: 'none' }); return }
|
|
|
+ var data = {
|
|
|
+ name: this.name.trim(),
|
|
|
+ organization: this.organization.trim(),
|
|
|
+ department: this.department.trim(),
|
|
|
+ title: this.title.trim(),
|
|
|
+ firstMeetScene: this.firstMeetScene.trim(),
|
|
|
+ knowWay: this.knowWay.trim(),
|
|
|
+ contactInfo: this.contactInfo.trim()
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ var res = await addOctopusKeyPerson(data)
|
|
|
+ if (res.code === 200) {
|
|
|
+ uni.showToast({ title: '保存成功', icon: 'success' })
|
|
|
+ setTimeout(() => uni.navigateBack(), 1500)
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.message || '保存失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ } catch (e) { uni.showToast({ title: '保存失败', icon: 'none' }) }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+<!-- 样式复用 octopus-add-effect.vue -->
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:注册 pages.json**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "path": "octopus-add-key-person",
|
|
|
+ "style": { "navigationBarTitleText": "添加关键人" }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages/action-detail/octopus-add-key-person.vue cfc-frontend/pages.json
|
|
|
+git commit -m "feat: octopus-add-key-person 关键人录入页(支持 prefill 预填)+ pages.json"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 9:前端新组件 OctopusDiagram.vue(全新实现,替换旧组件)
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`cfc-frontend/components/OctopusDiagram.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:实现 Canvas 2d 三层画布(参考 PearlDiagram/AbilityDiagram 模式)**
|
|
|
+
|
|
|
+关键要点:
|
|
|
+- `props: effects, keyPersons, expansions, selfId, canvasWidth, canvasHeight, interactive`
|
|
|
+- `data: dpr, ctx, _destroyed, _canvasNode, _canvasRect, canvasReady, centerNode, effectNodes, keyPersonNodes, expansionNodes, selectedNode, ...`
|
|
|
+- `mounted: $nextTick x2 → initCanvas(retry 5次) → buildLayout() → drawAll()`
|
|
|
+- `beforeDestroy: _destroyed = true`
|
|
|
+- `methods: initCanvas(cb), buildLayout(), drawAll(), _drawCenterNode, _drawEffectNodes, _drawKeyPersonNodes, _drawExpansionNodes, _drawEdges, _drawNode, hitTest, onTouchStart/Move/End, onCanvasTap, formatDate, _hexToRgba, _getCanvasRect`
|
|
|
+- **三层结构**:
|
|
|
+ 1. 中心 "我"
|
|
|
+ 2. 第1层:成效节点(圆形卡片,标签:金额大/频次高/新成交,点击 → 弹出关键人列表)
|
|
|
+ 3. 第2层:关键人节点(头像+姓名+单位/职务,边标记:引荐/决策,点击 → 弹窗完整画像)
|
|
|
+ 4. 第3层:拓展方向节点(虚线/浅色 ≤8个,点击 → 弹出"建议画像模板" → 跳转 octopus-add-key-person?prefill=...)
|
|
|
+- **交互**:
|
|
|
+ - `onTouchStart/Move/End` 区分滑动与点击(移动 >10px 取消点击)
|
|
|
+ - `hitTest` 圆形命中测试
|
|
|
+ - 点击成效节点 → `this.$emit('effect-click', effect)` 父组件处理弹出关键人列表
|
|
|
+ - 点击关键人 → 弹窗显示完整画像(单位/部门/职务/初识场合/如何认识/联系方式)
|
|
|
+ - 点击拓展节点 → `uni.navigateTo({ url: '/pages/action-detail/octopus-add-key-person?prefill=' + encodeURIComponent(JSON.stringify(prefill)) })`
|
|
|
+ - 长按/右上角 "重新分析" → `this.$emit('reanalyze')`
|
|
|
+
|
|
|
+- [ ] **步骤 2:样式(复用 PearlDiagram/AbilityDiagram 的 scoped CSS 模式)**
|
|
|
+- 禁 `?.`、禁 CSS Grid、禁 `:key` 表达式、日期格式化用 `substring(0,10)`
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/components/OctopusDiagram.vue
|
|
|
+git commit -m "feat: OctopusDiagram.vue 全新关键人拓展画布(三层 Canvas + 触摸交互 + 预填跳转)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 10:集成到 action-detail/index.vue
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages/action-detail/index.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:更新 imports + components 注册**
|
|
|
+
|
|
|
+```js
|
|
|
+// 删除旧 import
|
|
|
+// import OctopusDiagram from '../../components/OctopusDiagram.vue'
|
|
|
+// 新 import
|
|
|
+import OctopusDiagram from '../../components/OctopusDiagram.vue'
|
|
|
+import { getOctopusEffects, getOctopusAnalysisSummary, getOctopusExpansionSuggest } from '../../utils/api.js'
|
|
|
+```
|
|
|
+
|
|
|
+```js
|
|
|
+components: {
|
|
|
+ // 删 OctopusDiagram 旧引用,保留新组件
|
|
|
+ OctopusDiagram,
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:data 新增字段**
|
|
|
+
|
|
|
+```js
|
|
|
+data() {
|
|
|
+ return {
|
|
|
+ // 旧字段保留...
|
|
|
+ octopusEffects: null,
|
|
|
+ octopusKeyPersons: {}, // effectId -> [{...}, ...]
|
|
|
+ octopusExpansions: [],
|
|
|
+ ...
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:onShow 新增加载**
|
|
|
+
|
|
|
+```js
|
|
|
+onShow() {
|
|
|
+ this.loadOctopusData()
|
|
|
+ // 旧的 loadPearlResources/loadAbilityMap 保留
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:methods 新增**
|
|
|
+
|
|
|
+```js
|
|
|
+loadOctopusData() {
|
|
|
+ var self = this
|
|
|
+ getOctopusEffects().then(function(res) {
|
|
|
+ if (res.code === 200 && res.data) self.octopusEffects = res.data
|
|
|
+ })
|
|
|
+ getOctopusAnalysisSummary().then(function(res) {
|
|
|
+ if (res.code === 200 && res.data) self.octopusExpansions = res.data.directions || []
|
|
|
+ })
|
|
|
+},
|
|
|
+goOctopusAddEffect() { uni.navigateTo({ url: '/pages/action-detail/octopus-add-effect' }) },
|
|
|
+onEffectClick(effect) {
|
|
|
+ // 加载该成效的关键人
|
|
|
+ var self = this
|
|
|
+ getOctopusEffectKeyPersons({ effectId: effect.id }).then(function(res) {
|
|
|
+ if (res.code === 200) self.$set(self.octopusKeyPersons, effect.id, res.data)
|
|
|
+ })
|
|
|
+},
|
|
|
+onExpansionClick(expansion) {
|
|
|
+ // 预填跳转关键人录入页
|
|
|
+ var prefill = encodeURIComponent(JSON.stringify(expansion.prefill))
|
|
|
+ uni.navigateTo({ url: '/pages/action-detail/octopus-add-key-person?prefill=' + prefill })
|
|
|
+},
|
|
|
+onReanalyze() { this.loadOctopusData() }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:模板替换**
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- 旧:<OctopusDiagram :tentacles="tentacles" ... /> -->
|
|
|
+<!-- 新: -->
|
|
|
+<view class="section" v-if="isLoggedIn">
|
|
|
+ <view class="section-header">
|
|
|
+ <view>
|
|
|
+ <text class="section-title">🐙 关键人拓展</text>
|
|
|
+ <text class="section-sub">从成效出发,挖掘关键人,画出拓展路径</text>
|
|
|
+ </view>
|
|
|
+ <text class="section-link" @tap="goOctopusAddEffect">+ 记录成效</text>
|
|
|
+ </view>
|
|
|
+ <OctopusDiagram
|
|
|
+ ref="octopusDiagram"
|
|
|
+ :selfId="selfId"
|
|
|
+ :effects="octopusEffects?.list || octopusEffects"
|
|
|
+ :expansions="octopusExpansions"
|
|
|
+ :interactive="true"
|
|
|
+ @effect-click="onEffectClick"
|
|
|
+ @expansion-click="onExpansionClick"
|
|
|
+ @reanalyze="onReanalyze" />
|
|
|
+</view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages/action-detail/index.vue
|
|
|
+git commit -m "feat: action-detail 集成新 OctopusDiagram(三层画布 + 成效/关键人/拓展三层交互)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 11:API_REFERENCE.md 同步新接口
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`docs/superpowers/api/API_REFERENCE.md`
|
|
|
+
|
|
|
+- [ ] **步骤 1:Controller 总览表新增 KeyPersonController 行**
|
|
|
+
|
|
|
+```markdown
|
|
|
+| `KeyPersonController` | `/api/octopus` | 关键人拓展(成效/关键人/分析/拓展) | — |
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:在 4.39 后新增 4.40 节**
|
|
|
+
|
|
|
+```markdown
|
|
|
+### 4.40 关键人拓展章鱼图(`/api/octopus/*`)
|
|
|
+
|
|
|
+替代旧章鱼图(help_logs)。核心:成效记录 → 关键人画像 → 高频词分析 → 拓展方向。
|
|
|
+
|
|
|
+| 路径 | 说明 |
|
|
|
+|------|------|
|
|
|
+| `POST /api/octopus/effect/list` | 成效记录分页列表(支持 effect_type 筛选) |
|
|
|
+| `POST /api/octopus/effect/add` | 新增成效(member_order_id + effect_type bitmask + amount + desc) |
|
|
|
+| `POST /api/octopus/effect/delete` | 删除成效(级联关系) |
|
|
|
+| `POST /api/octopus/key-person/add` | 新增关键人(姓名/单位/部门/职务/初识场合/如何认识) |
|
|
|
+| `POST /api/octopus/key-person/update` | 修改关键人画像 |
|
|
|
+| `POST /api/octopus/key-person/delete` | 删除关键人(级联关系) |
|
|
|
+| `POST /api/octopus/effect/key-persons` | 某成效的关键人列表 |
|
|
|
+| `POST /api/octopus/analysis/keywords` | 高频词统计(单位/部门/职务/场合/路径) |
|
|
|
+| `POST /api/octopus/analysis/summary` | 高频词提炼 ≤8 个拓展方向 |
|
|
|
+| `POST /api/octopus/expansion/suggest` | 基于分析推荐关键人画像模板(预填) |
|
|
|
+
|
|
|
+> **已记录关键人拓展 API 端点 — 禁止重复注册。**
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:标记旧 OctopusController 接口废弃**
|
|
|
+
|
|
|
+在 "4.37 情绪识别" 后加旧接口废弃标记(见任务 5 步骤 3)。
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/superpowers/api/API_REFERENCE.md
|
|
|
+git commit -m "docs: API_REFERENCE 同步关键人拓展 10 接口 + 旧章鱼图 4 接口标记废弃"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 任务 12:全链路验证 + 最终 Commit
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 全部已修改文件
|
|
|
+
|
|
|
+- [ ] **步骤 1:后端编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -DskipTests
|
|
|
+# 预期:BUILD SUCCESS
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:前端语法检查**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-frontend && npm run lint 2>&1 | head -50
|
|
|
+# 无 Error 级别报错(Warning 可忽略)
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:API_REFERENCE 完整性检查**
|
|
|
+
|
|
|
+```bash
|
|
|
+grep -c "/api/octopus/" docs/superpowers/api/API_REFERENCE.md
|
|
|
+# 预期:包含 10 个新接口 + 4 个废弃接口标记
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:全量 Commit + Push**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add -A
|
|
|
+git commit -m "feat: 关键人拓展章鱼图全链路上线(迁移305-307 + 3实体/Mapper/Service/Controller + 新OctopusDiagram + 2新页面 + action-detail集成 + API文档同步 + 旧组件清理)"
|
|
|
+git push origin cfclub
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 执行方式选择
|
|
|
+
|
|
|
+**计划已完成并保存到 `docs/superpowers/plans/2026-09-10-key-person-expansion-octopus.md`。两种执行方式:**
|
|
|
+
|
|
|
+**1. 子代理驱动(推荐)** - 每个任务调度一个新的子代理,任务间进行审查,快速迭代
|
|
|
+> **必需子技能:** 使用 superpowers:subagent-driven-development
|
|
|
+> 每个任务一个新子代理 + 两阶段审查
|
|
|
+
|
|
|
+**2. 内联执行** - 在当前会话中使用 executing-plans 执行任务,批量执行并设有检查点供审查
|
|
|
+> **必需子技能:** 使用 superpowers:executing-plans
|
|
|
+> 批量执行并设有检查点供审查
|
|
|
+
|
|
|
+**选哪种方式?**
|