Extreme Programming--front-end and back-end separation contacts programming

832302122李炳言 2025-12-12 22:16:31

极限编程作业提交

一、作业基础信息表

项目EE308FZ 软件工程极限编程
小组成员李炳言
FZU ID832302122
MU ID23125942
作业要求完成通讯录项目开发,包含联系人收藏、多联系方式添加、导入导出等功能
作业目标掌握Git协作开发流程、Web项目部署、完成通讯录系统的基础与扩展功能

二、项目基础信息

2.1 项目地址

GitHub仓库地址具体地址
前端代码https://github.com/sananww29/832302122_contacts_frontend2
后端代码https://github.com/sananww29/832302122_contacts_backend2
补充代码https://github.com/sananww29/832302122_contacts_add

2.2 GitHub提交日志

  • 提交日志截图:

    img

  • 团队成员提交次数统计:
    团队成员提交次数(commit)主要提交内容(示例:功能开发、BUG修复、文档更新)
    李炳言1后端联系人收藏、多种联系方式、导入导出功能开发
    李炳言2多联系方式、收藏功能UI设计
    李炳言3数据库连接情况,导入导出功能测试,导出Excel格式优化

三、功能实现思路

3.1 联系人收藏功能

3.1.1 功能描述

实现用户对联系人的收藏/标记功能,用户可将特定联系人标记为重要联系人或常用联系人,以便后续快速查找访问。

3.1.2 技术选型

技术/框架版本用途选型理由
Spring Boot2.7.18后端框架提供完整的MVC架构和自动配置,简化开发
Spring Data JPA2.7.18数据访问简化数据库操作,支持对象关系映射
H2/MySQL-数据库H2用于快速开发测试,MySQL用于生产环境
RESTful API-前后端通信提供标准化的API接口,支持跨平台访问

3.1.3 核心逻辑

1. 数据模型设计

Contact实体中添加收藏字段:

@Entity
@Table(name = "contacts")
public class Contact {
    // 其他字段...
    
    @Column(nullable = false, columnDefinition = "boolean default false")
    private boolean isFavorite;
    
    // getter和setter方法...
}
2. API接口设计
接口路径方法功能描述请求体响应体
/api/contacts/{id}/favoritePATCH切换联系人收藏状态更新后的Contact对象
3. 业务逻辑实现
@Transactional
public Contact toggleFavorite(Long id) {
    // 查找联系人
    Contact contact = contactRepository.findById(id)
        .orElseThrow(() -> new IllegalArgumentException("联系人不存在: " + id));
    
    // 切换收藏状态
    contact.setFavorite(!contact.isFavorite());
    
    // 保存并返回更新后的联系人
    return contactRepository.save(contact);
}
4. 前端实现
// 切换收藏状态
async function toggleFavorite(id) {
  try {
    await fetchJSON(window.API_BASE + '/contacts/' + id + '/favorite', { method: 'PATCH' });
    loadList(); // 刷新列表
  } catch (e) {
    alert('切换收藏状态失败: ' + e.message);
  }
}

3.2 多联系方式添加功能

3.2.1 功能描述

支持为单个联系人关联多种联系方式,包括但不限于电话号码、电子邮箱、社交媒体账号、通讯地址等。

3.2.2 技术选型

技术/框架版本用途选型理由
Spring Boot2.7.18后端框架提供完整的MVC架构和自动配置
Spring Data JPA2.7.18数据访问支持一对多关系映射,简化数据库操作
RESTful API-前后端通信提供标准化的API接口
H2/MySQL-数据库支持关系型数据存储

3.3.3 核心逻辑

1. 数据模型设计

创建ContactInfo实体与Contact实体建立一对多关系:

@Entity
@Table(name = "contact_info")
public class ContactInfo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "contact_id", nullable = false)
    private Contact contact;
    
    @Column(nullable = false, length = 20)
    private String type; // 电话、邮箱、微信、QQ、地址等
    
    @Column(name = "contact_value", nullable = false, length = 100)
    private String value; // 联系方式的值
    
    @Column(name = "is_primary", nullable = false)
    private boolean isPrimary = false; // 是否为主联系方式
    
    // getter和setter方法...
}

Contact实体中添加联系方式集合:

@Entity
@Table(name = "contacts")
public class Contact {
    // 其他字段...
    
    @OneToMany(mappedBy = "contact", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
    private List<ContactInfo> contactInfos = new ArrayList<>();
    
    // getter和setter方法...
}
2. API接口设计
接口路径方法功能描述请求体响应体
/api/contactsPOST创建联系人(包含联系方式)ContactRequest创建后的Contact对象
/api/contacts/{id}PUT更新联系人(包含联系方式)ContactRequest更新后的Contact对象
3. 业务逻辑实现
@Transactional
public Contact create(ContactRequest req) {
    Contact contact = new Contact();
    contact.setName(req.getName());
    contact.setCategory(req.getCategory());
    contact.setFavorite(req.isFavorite());

    // 处理联系方式
    if (req.getContactInfos() != null && !req.getContactInfos().isEmpty()) {
        req.getContactInfos().forEach(infoReq -> {
            ContactInfo info = new ContactInfo();
            info.setContact(contact);
            info.setType(infoReq.getType());
            info.setValue(infoReq.getValue());
            info.setPrimary(infoReq.isPrimary());
            contact.getContactInfos().add(info);
        });
    }

    return contactRepository.save(contact);
}
4. 前端实现
// 动态添加联系方式表单
function addContactInfoItem() {
  // 创建新的联系方式输入项
  const container = document.getElementById('contactInfosContainer');
  const newItem = document.createElement('div');
  newItem.className = 'contact-info-item';
  newItem.innerHTML = `
    <select class="contact-type">
      <option value="手机号">手机号</option>
      <option value="邮箱">邮箱</option>
      <option value="微信">微信</option>
      <option value="QQ">QQ</option>
      <option value="地址">地址</option>
    </select>
    <input type="text" class="contact-value" placeholder="联系方式值" />
    <input type="checkbox" class="contact-primary" /> 设为主联系方式
    <button class="remove-contact">删除</button>
  `;
  container.insertBefore(newItem, document.getElementById('addContactBtn'));
}

// 收集联系方式数据
function collectContactInfos() {
  const items = document.querySelectorAll('.contact-info-item');
  const contactInfos = [];
  
  items.forEach(item => {
    const type = item.querySelector('.contact-type').value;
    const value = item.querySelector('.contact-value').value;
    const isPrimary = item.querySelector('.contact-primary').checked;
    
    if (value) {
      contactInfos.push({ type, value, primary: isPrimary });
    }
  });
  
  return contactInfos;
}

3.3 导入导出功能

3.3.1 功能描述

实现通讯录数据的导入与导出功能:

  • 导出:将通讯录中所有联系人的完整信息导出至Excel表格,格式规范需满足"一人一行、一信息一列"
  • 导入:支持读取符合规范格式的Excel文件,并将其中的联系人信息批量添加至通讯录

3.3.2 技术选型

技术/框架版本用途选型理由
Apache POI5.2.5Excel处理提供强大的Excel文件读写能力,支持.xlsx和.xls格式
Spring Boot2.7.18后端框架提供完整的MVC架构和文件上传支持
RESTful API-前后端通信提供标准化的API接口,支持文件上传下载
JavaScript Blob-前端文件处理支持在浏览器中处理二进制文件,实现文件下载

3.3.3 核心逻辑

1 导出功能

API接口设计

  • 路径:/api/contacts/export
  • 方法:GET
  • 响应:Excel文件流

核心逻辑实现

@Transactional(readOnly = true)
public ByteArrayInputStream exportContactsToExcel() throws IOException {
    String[] headers = {"ID", "姓名", "分类", "是否收藏", "联系方式类型", "联系方式值", "是否主联系方式"};

    // 创建工作簿
    Workbook workbook = new XSSFWorkbook();
    Sheet sheet = workbook.createSheet("联系人列表");

    // 创建表头
    Row headerRow = sheet.createRow(0);
    for (int col = 0; col < headers.length; col++) {
        Cell cell = headerRow.createCell(col);
        cell.setCellValue(headers[col]);
        // 设置表头样式...
    }

    // 获取所有联系人
    List<Contact> contacts = contactRepository.findAll();

    // 填充数据
    int rowIdx = 1;
    for (Contact contact : contacts) {
        // 为每个联系方式创建一行
        for (ContactInfo info : contact.getContactInfos()) {
            Row row = sheet.createRow(rowIdx++);
            row.createCell(0).setCellValue(contact.getId());
            row.createCell(1).setCellValue(contact.getName());
            row.createCell(2).setCellValue(contact.getCategory() != null ? contact.getCategory() : "");
            row.createCell(3).setCellValue(contact.isFavorite() ? "是" : "否");
            row.createCell(4).setCellValue(info.getType());
            row.createCell(5).setCellValue(info.getValue());
            row.createCell(6).setCellValue(info.isPrimary() ? "是" : "否");
        }
    }

    // 设置列宽
    for (int col = 0; col < headers.length; col++) {
        sheet.autoSizeColumn(col);
    }

    // 将工作簿转换为字节数组
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    workbook.write(outputStream);
    workbook.close();

    return new ByteArrayInputStream(outputStream.toByteArray());
}

前端实现

async function exportContacts() {
  try {
    const response = await fetchJSON(window.API_BASE + '/contacts/export', {
      method: 'GET',
      responseType: 'blob'
    });
    
    // 创建下载链接
    const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `通讯录_${new Date().toISOString().split('T')[0]}.xlsx`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    window.URL.revokeObjectURL(url);
  } catch (error) {
    console.error('导出联系人失败:', error);
    alert('导出联系人失败,请稍后重试');
  }
}
2 导入功能

API接口设计

  • 路径:/api/contacts/import
  • 方法:POST
  • 请求:MultipartFile(Excel文件)
  • 响应:导入的联系人列表

核心逻辑实现

@Transactional
public List<Contact> importContactsFromExcel(MultipartFile file) throws IOException {
    List<Contact> importedContacts = new ArrayList<>();

    // 创建工作簿
    Workbook workbook = WorkbookFactory.create(file.getInputStream());
    Sheet sheet = workbook.getSheetAt(0);

    // 遍历所有行(跳过表头)
    Iterator<Row> rows = sheet.iterator();
    rows.next(); // 跳过表头

    while (rows.hasNext()) {
        Row currentRow = rows.next();

        // 获取单元格数据
        String name = getCellValue(currentRow.getCell(1));
        String category = getCellValue(currentRow.getCell(2));
        boolean isFavorite = "是".equals(getCellValue(currentRow.getCell(3)));
        String contactType = getCellValue(currentRow.getCell(4));
        String contactValue = getCellValue(currentRow.getCell(5));
        boolean isPrimary = "是".equals(getCellValue(currentRow.getCell(6)));

        // 数据验证
        if (name == null || name.trim().isEmpty() || contactType == null || contactType.trim().isEmpty() || contactValue == null || contactValue.trim().isEmpty()) {
            continue;
        }

        // 查找是否已存在相同姓名的联系人
        Contact contact = importedContacts.stream()
                .filter(c -> name.equals(c.getName()))
                .findFirst()
                .orElse(null);

        // 如果联系人不存在,创建新联系人
        if (contact == null) {
            contact = new Contact();
            contact.setName(name);
            contact.setCategory(category);
            contact.setFavorite(isFavorite);
            importedContacts.add(contact);
        }

        // 添加联系方式
        ContactInfo contactInfo = new ContactInfo();
        contactInfo.setContact(contact);
        contactInfo.setType(contactType);
        contactInfo.setValue(contactValue);
        contactInfo.setPrimary(isPrimary);
        contact.getContactInfos().add(contactInfo);
    }

    // 保存所有联系人到数据库
    return contactRepository.saveAll(importedContacts);
}

// 获取单元格值的辅助方法
private String getCellValue(Cell cell) {
    if (cell == null) {
        return null;
    }

    switch (cell.getCellType()) {
        case STRING: return cell.getStringCellValue();
        case BOOLEAN: return String.valueOf(cell.getBooleanCellValue());
        case NUMERIC: return String.valueOf(cell.getNumericCellValue());
        default: return null;
    }
}

前端实现

async function importContacts(event) {
  const file = event.target.files[0];
  if (!file) return;
  
  // 验证文件类型
  const validTypes = ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel'];
  if (!validTypes.includes(file.type)) {
    alert('请选择Excel文件(.xlsx或.xls格式)');
    return;
  }
  
  const formData = new FormData();
  formData.append('file', file);
  
  try {
    await fetchJSON(window.API_BASE + '/contacts/import', {
      method: 'POST',
      body: formData,
      headers: {
        // 不需要设置Content-Type,浏览器会自动设置为multipart/form-data
      }
    });
    
    alert('联系人导入成功');
    loadList(); // 刷新联系人列表
    event.target.value = ''; // 重置文件输入
  } catch (error) {
    console.error('导入联系人失败:', error);
    alert('导入联系人失败,请检查文件格式并稍后重试');
  }
}

3.4 系统架构图

+----------------+       +----------------+       +----------------+
|   前端页面     |       |   RESTful API  |       |   后端服务     |
|  (HTML/CSS/JS) |<----->|   (Spring MVC) |<----->|  (Spring Boot) |
+----------------+       +----------------+       +----------------+
                                                      |
                                                      v
                                              +----------------+
                                              |   数据库       |
                                              |  (H2/MySQL)    |
                                              +----------------+

四、程序运行展示

首页/联系人列表页:

img

4.1 联系人收藏功能页

当我们想对某个联系人实现收藏 / 标记功能

img

点击收藏栏的星星图标即可在该栏进行标黄标记。

img

4.2多联系方式添加页

我们设计了“手机号”、“微信”、“QQ”、“邮箱”、“地址”五种联系方式

img

点击“添加联系方式”按键,可以给一个联系人添加多种联系方式

img


img

并且可以设置其中一个为主联系方式

img

点击联系人后方的修改键也可以增加或删除联系方式

img

当只剩下一个联系方式时,将不能再进行删除,并且会跳出来“至少需要保留一个联系方式”的提示

img

4.3 导入导出功能页

点击界面中的“导出通讯录”

img

即可直接在浏览器中下载通讯录现有名单的excel表格

img

包含每个联系人的"ID", "姓名", "分类", "是否收藏", "联系方式类型", "联系方式值", "是否主联系方式"元素,示范如下

img

也可导入刚刚导出的通讯录文件,直接在电脑中选择我们刚刚导出的文件

img

成功的话会显示“联系人导入成功”

img

再点击“确认”即可看到成功导入的通讯录文件

img

五、团队分工与贡献度评估

5.1 团队分工

项目的全部内容为独立完成

  1. 联系人列表页与收藏页UI实现;
  2. 收藏状态切换逻辑开发;
  3. 提交日志管理;
  4. 数据库表设计(联系人表、联系方式表);
  5. 导入导出接口开发

5.2 贡献度评估

成员贡献领域贡献占比贡献说明
李炳言前端开发、后端开发、导入导出、收藏功能实现100%独立完成前端核心页面开发,完成数据库设计与后端接口,实现导入导出功能,解决收藏状态同步问题,提交次数3次

六、协作问题与解决方案

  • 遇到的问题1:前端收藏状态与后端数据不同步
    • 解决方案:在页面加载时调用“获取收藏状态”接口,重新渲染标记;优化提交逻辑,确保状态变更后即时同步后端
  • 遇到的问题2:刷新页面后收藏标记消失
    • 解决方案:优化提交逻辑,确保状态变更后即时同步后端
  • 遇到的问题3:导入Excel表格时,不同版本Excel格式解析失败
    • 解决方案:使用xlsx.js统一解析逻辑,添加版本兼容处理,测试.xls与.xlsx格式文件

七、PSP表格

任务类别计划耗时(分钟)实际耗时(分钟)偏差原因分析
需求分析(功能拆解)6080对“多联系方式”的边界场景考虑不足,额外花费时间梳理需求
技术选型与设计9090提前调研技术方案,设计过程顺利
前端开发(收藏功能等)240300遇到收藏状态同步问题,调试耗时较长
Git提交与协作管理3040分支合并冲突处理花费额外时间
数据库设计60180前端后端接口对不上,反复调试检查代码花费大量时间
后端接口开发(导入导出)180210Excel解析格式兼容问题调试耗时
功能测试与BUG修复6080发现导入数据重复问题,需添加去重逻辑
文档撰写(博客/作业)6060按模板整理内容,效率较高
总计7801040主要偏差来自数据库连接、功能调试与冲突处理
...全文
42 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

164

社区成员

发帖
与我相关
我的任务
社区描述
2501_MU_SE_FZU
软件工程 高校
社区管理员
  • FZU_SE_LQF
  • 助教_林日臻
  • 朱仕君
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧