# 行远页 · 重要关系维护 — 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. > > **Goal:** 在行远(行动)TabBar 页新增「重要关系」维护功能,支持通讯录导入、关系管理、亲密度追踪 > > **Design Spec:** `docs/superpowers/specs/2026-06-14-action-page-relationship-design.md` --- ## 文件清单 ### 新增 | 文件 | 类型 | 说明 | |------|:----:|------| | `cfc-backend/.../entity/Contact.java` | entity | 通讯录关系实体 | | `cfc-backend/.../dto/ContactDTO.java` | dto | 关系传输对象 | | `cfc-backend/.../mapper/ContactMapper.java` | mapper | MyBatis Mapper | | `cfc-backend/.../service/ContactService.java` | service | 关系 CRUD + 导入逻辑 | | `cfc-backend/.../controller/ContactController.java` | controller | 关系 API | | `cfc-frontend/components/ContactCard.vue` | component | 关系卡片组件 | | `cfc-frontend/components/ContactImport.vue` | component | 通讯录导入浮层 | | `cfc-frontend/pages/action/contact-detail.vue` | page | 关系详情/编辑页 | ### 修改 | 文件 | 说明 | |------|------| | `cfc-frontend/pages/action/index.vue` | 插入「重要关系」区块(登录后可见) | | `cfc-frontend/pages.json` | 注册 contact-detail 页面路径 | | `cfc-frontend/utils/api.js` | 新增 contact API 封装 | | `cfc-backend/.../resources/mapper/ContactMapper.xml` | SQL 映射文件 | --- ## Task 1: 后端 Contact 实体 + Mapper **目的:** 创建数据库表和 ORM 映射 **Step 1: 创建 Contact.java** ```java package com.etotem.cfc.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @TableName("contacts") public class Contact implements Serializable { @TableId(type = IdType.AUTO) private Long id; private Long userId; private String name; private String phone; private String avatar; private String relationshipType; // family/friend/partner/colleague/other private Date knownSince; private String bio; private Date birthday; private Integer intimacyLevel; private String contactSource; // phone/manual private String tags; private Date lastContactAt; private Integer contactCount; private String notes; private Date createdAt; private Date updatedAt; } ``` **Step 2: 创建 ContactMapper.java** ```java @Mapper public interface ContactMapper extends BaseMapper {} ``` **Step 3: 创建 ContactMapper.xml** MyBatis XML 映射文件,标准 CRUD。 **Step 4: 创建 ContactDTO.java** ```java @Data public class ContactDTO { private Long id; private Long userId; private String name; private String phone; private String avatar; private String relationshipType; private Date knownSince; private String knownSinceLabel; // "认识5年" private String bio; private Date birthday; private String birthdayLabel; // "06月15日" private String birthdayCountdown; // "还有3天" / null private Integer intimacyLevel; private String contactSource; private String tags; private Date lastContactAt; private Integer contactCount; private String notes; private Date createdAt; private Date updatedAt; } ``` **验证:** `mvn clean compile` --- ## Task 2: 后端 ContactService + ContactController **目的:** 实现 CRUD 和通讯录导入逻辑 **Step 1: ContactService.java** ```java @Service public class ContactService { @Resource private ContactMapper contactMapper; // 获取当前用户的所有联系人 public Result> list(Long userId) { LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(Contact::getUserId, userId) .orderByDesc(Contact::getIntimacyLevel); List list = contactMapper.selectList(query); return Result.success(toDTOList(list)); } // 创建联系人 public Result create(Contact contact, Long userId) { contact.setUserId(userId); contact.setContactSource("manual"); contact.setIntimacyLevel(calcIntimacy(contact)); contact.setContactCount(0); contact.setCreatedAt(new Date()); contact.setUpdatedAt(new Date()); contactMapper.insert(contact); return Result.success(toDTO(contact)); } // 从通讯录导入 public Result importFromPhone(String name, String phone, Map extra, Long userId) { // 查重 LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(Contact::getUserId, userId) .eq(Contact::getPhone, phone); Contact existing = contactMapper.selectOne(query); if (existing != null) { return Result.error("该联系人已在您的关系列表中"); } Contact contact = new Contact(); contact.setUserId(userId); contact.setName(name); contact.setPhone(phone); contact.setContactSource("phone"); if (extra != null) { if (extra.get("relationshipType") != null) contact.setRelationshipType((String) extra.get("relationshipType")); if (extra.get("knownSince") != null) contact.setKnownSince((Date) extra.get("knownSince")); if (extra.get("birthday") != null) contact.setBirthday((Date) extra.get("birthday")); if (extra.get("bio") != null) contact.setBio((String) extra.get("bio")); } contact.setIntimacyLevel(30); // 初始值 contact.setContactCount(0); contact.setCreatedAt(new Date()); contact.setUpdatedAt(new Date()); contactMapper.insert(contact); return Result.success(toDTO(contact)); } // 标记互动 public Result recordInteraction(Long id, Long userId) { Contact contact = contactMapper.selectById(id); if (contact == null || !contact.getUserId().equals(userId)) { return Result.error("联系人不存在"); } contact.setContactCount(contact.getContactCount() == null ? 1 : contact.getContactCount() + 1); contact.setLastContactAt(new Date()); contact.setIntimacyLevel(calcIntimacy(contact)); contact.setUpdatedAt(new Date()); contactMapper.updateById(contact); return Result.success(toDTO(contact)); } // 亲密度计算 private int calcIntimacy(Contact c) { int base = Math.min((c.getContactCount() == null ? 0 : c.getContactCount()) * 5, 60); int active = 0; if (c.getLastContactAt() != null) { long days = (System.currentTimeMillis() - c.getLastContactAt().getTime()) / 86400000L; if (days <= 7) active = 20; else if (days <= 30) active = 10; else if (days <= 90) active = 5; else if (days > 365) active = -10; } return Math.max(0, Math.min(100, base + active)); } } ``` **Step 2: ContactController.java** ```java @RestController @RequestMapping("/api/contact") public class ContactController { @Resource private ContactService contactService; @PostMapping("/list") public Result> list(@RequestAttribute("userId") Long userId) { return contactService.list(userId); } @PostMapping("/create") public Result create(@RequestBody Contact contact, @RequestAttribute("userId") Long userId) { return contactService.create(contact, userId); } @PostMapping("/update") public Result update(@RequestBody Contact contact, @RequestAttribute("userId") Long userId) { return contactService.update(contact, userId); } @PostMapping("/delete") public Result delete(@RequestBody Map params, @RequestAttribute("userId") Long userId) { Long id = Long.valueOf(params.get("id").toString()); return contactService.delete(id, userId); } @PostMapping("/detail") public Result detail(@RequestBody Map params, @RequestAttribute("userId") Long userId) { Long id = Long.valueOf(params.get("id").toString()); return contactService.detail(id, userId); } @PostMapping("/import-phone") public Result importPhone(@RequestBody Map params, @RequestAttribute("userId") Long userId) { String name = (String) params.get("name"); String phone = (String) params.get("phone"); Map extra = (Map) params.get("extra"); return contactService.importFromPhone(name, phone, extra, userId); } @PostMapping("/interaction") public Result interaction(@RequestBody Map params, @RequestAttribute("userId") Long userId) { Long id = Long.valueOf(params.get("id").toString()); return contactService.recordInteraction(id, userId); } } ``` **验证:** `mvn clean compile` + 启动测试接口 --- ## Task 3: 前端 API 封装 **目的:** 在 api.js 中新增 contact API **修改:** `cfc-frontend/utils/api.js` ```javascript // ===== 通讯录关系(行远页) ===== export const getContactList = () => request('/api/contact/list', 'POST') export const createContact = (data) => request('/api/contact/create', 'POST', data) export const updateContact = (data) => request('/api/contact/update', 'POST', data) export const deleteContact = (data) => request('/api/contact/delete', 'POST', data) export const getContactDetail = (id) => request('/api/contact/detail', 'POST', { id }) export const importPhoneContact = (data) => request('/api/contact/import-phone', 'POST', data) export const recordContactInteraction = (id) => request('/api/contact/interaction', 'POST', { id }) ``` **验证:** 确保 api.js 语法无异常 --- ## Task 4: ContactCard 组件 **目的:** 关系卡片组件,用于行远页横向滚动列表 **创建:** `cfc-frontend/components/ContactCard.vue` ```vue ``` --- ## Task 5: ContactImport 浮层组件 **目的:** 通讯录导入后的补充信息填写浮层 **创建:** `cfc-frontend/components/ContactImport.vue` 交互流程: 1. 用户调用 `wx.chooseContact`(需在点击事件中触发) 2. 获取 name + phone 3. 组件弹窗显示输入表单:关系类型(picker)、认识时间(picker)、生日(picker)、个人介绍(input) 4. 点击确认 → 调用 `importPhoneContact({ name, phone, extra: ... })` 5. 成功 → 关闭浮层 → 触发父组件刷新列表 ```vue ``` --- ## Task 6: 改造行远页 — 插入关系区块 **目的:** 在行远页「今日待办」和「热门活动」之间插入「重要关系」区块 **修改:** `cfc-frontend/pages/action/index.vue` **模板修改(登录后区块间插入):** ```diff + + + + 🔥 重要关系 + + ? + 📱 导入通讯录 + + + + + + + + + + + 📇 + 还没有记录重要关系 + + + + + + + ``` **Script 新增:** ```javascript // data 新增: contacts: [], // methods 新增: loadContacts: function() { var self = this getContactList().then(function(res) { if (res.code === 200 && res.data) { self.contacts = res.data.slice(0, 10) } }).catch(function() { self.contacts = [] }) }, importContact: function() { var self = this // 微信小程序: 选择联系人 uni.chooseContact({ success: function(res) { self.pendingImport = { name: res.name, phone: res.phone } self.showImportModal = true }, fail: function() { // 手动输入 fallback uni.showModal({ title: '导入失败', content: '可选择手动添加', success: function(r) { if (r.confirm) self.manualAdd() } }) } }) }, manualAdd: function() { uni.navigateTo({ url: '/pages/action/contact-detail?mode=create' }) }, goContactDetail: function(id) { uni.navigateTo({ url: '/pages/action/contact-detail?id=' + id }) } ``` **onShow 新增加载:** ```javascript if (this.isLoggedIn) { this.loadContacts() } ``` --- ## Task 7: 关系详情/编辑页 **目的:** 查看联系人完整信息 + 编辑 + 标记互动 **创建:** `cfc-frontend/pages/action/contact-detail.vue` **布局:** ``` ┌─ 返回 ── 关系详情 ───── [编辑] ───┐ │ │ │ [头像] 张三 │ │ 朋友 · 认识5年 │ │ ❤️ 亲密度 85 │ │ │ ├─ 基本信息 ─────────────────────────┤ │ 📞 138xxxx1234 │ │ 🎂 06月15日 │ │ 📝 个人介绍/备注 │ │ 🏷️ 标签 │ │ 💬 备注 │ │ │ ├─ 互动记录 ─────────────────────────┤ │ 2026-06-10 查看了资料 │ │ 2026-05-20 更新了生日 │ │ [📞 标记联系] │ │ │ ├─ 操作 ─────────────────────────────┤ │ [删除联系人] │ └────────────────────────────────────┘ ``` **编辑模式:** 点击[编辑] → 字段变为可编辑 → 保存调用 `updateContact` --- ## Task 8: 页面注册 + 配置 **修改:** `cfc-frontend/pages.json` ```json { "path": "pages/action/contact-detail", "style": { "navigationBarTitleText": "关系详情" } } ``` --- ## 执行顺序 ``` Task 1 (Contact实体+Mapper) → Task 2 (Service+Controller) ↓ Task 3 (前端API封装) ↓ ┌─────────────────────┼─────────────────────┐ ↓ ↓ ↓ Task 4 (ContactCard) Task 5 (ContactImport) Task 7 (详情页) ↓ ↓ ↓ └─────────────────────┼─────────────────────┘ ↓ Task 6 (行远页改造) ↓ Task 8 (pages.json注册) ``` **推荐并行:** Task 1 + Task 4 + Task 5 可同时执行(前后端无依赖)。 Task 2 依赖 Task 1。Task 3 依赖 Task 2。Task 6 依赖 Task 3 + Task 4 + Task 5。