面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(
- [ ])语法来跟踪进度。
目标: 替换现有"帮助记录型章鱼图"(contact_help_logs + OctopusController + OctopusDiagram.vue),上线"关键人拓展型章鱼图":成效记录(会员客户) → 类型bitmask(金额大/频次高/新成交) → 关键人画像(单位/部门/职务/初识场合/如何认识) → 四步法 + 高频词分析 → ≤8 个拓展方向。
架构:
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。
文件:
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java:10549(在迁移304后追加)修改:cfc-backend/src/main/resources/schema.sql(在 resource_items 表后追加 3 表)
[ ] 步骤 1:编写失败的测试(验证迁移不存在)
# 验证当前无这 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
// 迁移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 表后追加)
-- 关键人拓展-成效记录
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:验证编译
cd cfc-backend && mvn clean compile -DskipTests
# 预期:BUILD SUCCESS
[ ] 步骤 5:Commit
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)"
文件:
cfc-backend/src/main/java/com/etotem/cfc/entity/OctopusEffectRecord.javacfc-backend/src/main/java/com/etotem/cfc/entity/OctopusKeyPerson.javacfc-backend/src/main/java/com/etotem/cfc/entity/OctopusEffectKeyPerson.javacfc-backend/src/main/java/com/etotem/cfc/mapper/OctopusEffectRecordMapper.javacfc-backend/src/main/java/com/etotem/cfc/mapper/OctopusKeyPersonMapper.java创建:cfc-backend/src/main/java/com/etotem/cfc/mapper/OctopusEffectKeyPersonMapper.java
[ ] 步骤 1:OctopusEffectRecord.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
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
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)
// 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> {
}
// 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> {
}
// 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
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)"
文件:
创建:cfc-backend/src/main/java/com/etotem/cfc/service/KeyPersonService.java
[ ] 步骤 1:编写失败的测试(验证类不存在)
ls cfc-backend/src/main/java/com/etotem/cfc/service/KeyPersonService.java
# 预期:No such file
[ ] 步骤 2:实现 KeyPersonService.java(参考 ResourceService 模式)
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
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 核心业务逻辑(四步法全接口实现)"
文件:
创建:cfc-backend/src/main/java/com/etotem/cfc/controller/KeyPersonController.java
[ ] 步骤 1:实现 Controller(完全遵循 PearlController 模式)
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
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)"
文件:
cfc-backend/src/main/java/com/etotem/cfc/controller/OctopusController.javacfc-frontend/components/OctopusDiagram.vuecfc-frontend/pages/action-detail/octopus-add-help.vuecfc-frontend/pages/action-detail/octopus-records.vuecfc-frontend/pages.json(移除 octopus-add-help, octopus-records 路由)cfc-frontend/utils/api.js(移除旧 4 个 octopus 封装)修改:docs/superpowers/api/API_REFERENCE.md(标记旧接口废弃)
[ ] 步骤 1:OctopusController 标记 410
// 修改每个方法第一行返回 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
# 删除文件
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
cd cfc-backend && mvn clean compile -DskipTests
git add -A
git commit -m "refactor: 废弃旧章鱼图接口/组件/页面/文档(410 Gone),清理 3 旧文件"
文件:
修改:cfc-frontend/utils/api.js(在 getAbilityMap 后追加)
[ ] 步骤 1:追加 10 个函数
// ===== 关键人拓展章鱼图 =====
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
git add cfc-frontend/utils/api.js
git commit -m "feat: api.js 新增 10 个关键人拓展接口封装"
文件:
创建:cfc-frontend/pages/action-detail/octopus-add-effect.vue
[ ] 步骤 1:参考 pearl-add-resource.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
// pages.json action-detail 分包中,在 pearl-add-resource 后加:
{
"path": "octopus-add-effect",
"style": { "navigationBarTitleText": "记录成效" }
}
[ ] 步骤 3:Commit
git add cfc-frontend/pages/action-detail/octopus-add-effect.vue cfc-frontend/pages.json
git commit -m "feat: octopus-add-effect 成效录入页 + pages.json 注册"
文件:
创建:cfc-frontend/pages/action-detail/octopus-add-key-person.vue
[ ] 步骤 1:实现(支持预填 prefill 参数)
<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
{
"path": "octopus-add-key-person",
"style": { "navigationBarTitleText": "添加关键人" }
}
[ ] 步骤 3:Commit
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"
文件:
创建:cfc-frontend/components/OctopusDiagram.vue
[ ] 步骤 1:实现 Canvas 2d 三层画布(参考 PearlDiagram/AbilityDiagram 模式)
关键要点:
props: effects, keyPersons, expansions, selfId, canvasWidth, canvasHeight, interactivedata: dpr, ctx, _destroyed, _canvasNode, _canvasRect, canvasReady, centerNode, effectNodes, keyPersonNodes, expansionNodes, selectedNode, ...mounted: $nextTick x2 → initCanvas(retry 5次) → buildLayout() → drawAll()beforeDestroy: _destroyed = truemethods: initCanvas(cb), buildLayout(), drawAll(), _drawCenterNode, _drawEffectNodes, _drawKeyPersonNodes, _drawExpansionNodes, _drawEdges, _drawNode, hitTest, onTouchStart/Move/End, onCanvasTap, formatDate, _hexToRgba, _getCanvasRect交互:
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
git add cfc-frontend/components/OctopusDiagram.vue
git commit -m "feat: OctopusDiagram.vue 全新关键人拓展画布(三层 Canvas + 触摸交互 + 预填跳转)"
文件:
修改:cfc-frontend/pages/action-detail/index.vue
[ ] 步骤 1:更新 imports + components 注册
// 删除旧 import
// import OctopusDiagram from '../../components/OctopusDiagram.vue'
// 新 import
import OctopusDiagram from '../../components/OctopusDiagram.vue'
import { getOctopusEffects, getOctopusAnalysisSummary, getOctopusExpansionSuggest } from '../../utils/api.js'
components: {
// 删 OctopusDiagram 旧引用,保留新组件
OctopusDiagram,
...
}
[ ] 步骤 2:data 新增字段
data() {
return {
// 旧字段保留...
octopusEffects: null,
octopusKeyPersons: {}, // effectId -> [{...}, ...]
octopusExpansions: [],
...
}
}
[ ] 步骤 3:onShow 新增加载
onShow() {
this.loadOctopusData()
// 旧的 loadPearlResources/loadAbilityMap 保留
}
[ ] 步骤 4:methods 新增
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:模板替换
<!-- 旧:<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
git add cfc-frontend/pages/action-detail/index.vue
git commit -m "feat: action-detail 集成新 OctopusDiagram(三层画布 + 成效/关键人/拓展三层交互)"
文件:
修改:docs/superpowers/api/API_REFERENCE.md
[ ] 步骤 1:Controller 总览表新增 KeyPersonController 行
| `KeyPersonController` | `/api/octopus` | 关键人拓展(成效/关键人/分析/拓展) | — |
[ ] 步骤 2:在 4.39 后新增 4.40 节
### 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
git add docs/superpowers/api/API_REFERENCE.md
git commit -m "docs: API_REFERENCE 同步关键人拓展 10 接口 + 旧章鱼图 4 接口标记废弃"
文件:
全部已修改文件
[ ] 步骤 1:后端编译验证
cd cfc-backend && mvn clean compile -DskipTests
# 预期:BUILD SUCCESS
[ ] 步骤 2:前端语法检查
cd cfc-frontend && npm run lint 2>&1 | head -50
# 无 Error 级别报错(Warning 可忽略)
[ ] 步骤 3:API_REFERENCE 完整性检查
grep -c "/api/octopus/" docs/superpowers/api/API_REFERENCE.md
# 预期:包含 10 个新接口 + 4 个废弃接口标记
[ ] 步骤 4:全量 Commit + Push
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 批量执行并设有检查点供审查
选哪种方式?