164
社区成员
发帖
与我相关
我的任务
分享| 项目 | EE308FZ 软件工程极限编程 |
|---|---|
| 小组成员 | 李炳言 |
| FZU ID | 832302122 |
| MU ID | 23125942 |
| 作业要求 | 完成通讯录项目开发,包含联系人收藏、多联系方式添加、导入导出等功能 |
| 作业目标 | 掌握Git协作开发流程、Web项目部署、完成通讯录系统的基础与扩展功能 |
| GitHub仓库地址 | 具体地址 |
|---|---|
| 前端代码 | https://github.com/sananww29/832302122_contacts_frontend2 |
| 后端代码 | https://github.com/sananww29/832302122_contacts_backend2 |
| 补充代码 | https://github.com/sananww29/832302122_contacts_add |

| 团队成员 | 提交次数(commit) | 主要提交内容(示例:功能开发、BUG修复、文档更新) |
|---|---|---|
| 李炳言 | 1 | 后端联系人收藏、多种联系方式、导入导出功能开发 |
| 李炳言 | 2 | 多联系方式、收藏功能UI设计 |
| 李炳言 | 3 | 数据库连接情况,导入导出功能测试,导出Excel格式优化 |
实现用户对联系人的收藏/标记功能,用户可将特定联系人标记为重要联系人或常用联系人,以便后续快速查找访问。
| 技术/框架 | 版本 | 用途 | 选型理由 |
|---|---|---|---|
| Spring Boot | 2.7.18 | 后端框架 | 提供完整的MVC架构和自动配置,简化开发 |
| Spring Data JPA | 2.7.18 | 数据访问 | 简化数据库操作,支持对象关系映射 |
| H2/MySQL | - | 数据库 | H2用于快速开发测试,MySQL用于生产环境 |
| RESTful API | - | 前后端通信 | 提供标准化的API接口,支持跨平台访问 |
在Contact实体中添加收藏字段:
@Entity
@Table(name = "contacts")
public class Contact {
// 其他字段...
@Column(nullable = false, columnDefinition = "boolean default false")
private boolean isFavorite;
// getter和setter方法...
}
| 接口路径 | 方法 | 功能描述 | 请求体 | 响应体 |
|---|---|---|---|---|
/api/contacts/{id}/favorite | PATCH | 切换联系人收藏状态 | 无 | 更新后的Contact对象 |
@Transactional
public Contact toggleFavorite(Long id) {
// 查找联系人
Contact contact = contactRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("联系人不存在: " + id));
// 切换收藏状态
contact.setFavorite(!contact.isFavorite());
// 保存并返回更新后的联系人
return contactRepository.save(contact);
}
// 切换收藏状态
async function toggleFavorite(id) {
try {
await fetchJSON(window.API_BASE + '/contacts/' + id + '/favorite', { method: 'PATCH' });
loadList(); // 刷新列表
} catch (e) {
alert('切换收藏状态失败: ' + e.message);
}
}
支持为单个联系人关联多种联系方式,包括但不限于电话号码、电子邮箱、社交媒体账号、通讯地址等。
| 技术/框架 | 版本 | 用途 | 选型理由 |
|---|---|---|---|
| Spring Boot | 2.7.18 | 后端框架 | 提供完整的MVC架构和自动配置 |
| Spring Data JPA | 2.7.18 | 数据访问 | 支持一对多关系映射,简化数据库操作 |
| RESTful API | - | 前后端通信 | 提供标准化的API接口 |
| H2/MySQL | - | 数据库 | 支持关系型数据存储 |
创建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方法...
}
| 接口路径 | 方法 | 功能描述 | 请求体 | 响应体 |
|---|---|---|---|---|
/api/contacts | POST | 创建联系人(包含联系方式) | ContactRequest | 创建后的Contact对象 |
/api/contacts/{id} | PUT | 更新联系人(包含联系方式) | ContactRequest | 更新后的Contact对象 |
@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);
}
// 动态添加联系方式表单
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;
}
实现通讯录数据的导入与导出功能:
| 技术/框架 | 版本 | 用途 | 选型理由 |
|---|---|---|---|
| Apache POI | 5.2.5 | Excel处理 | 提供强大的Excel文件读写能力,支持.xlsx和.xls格式 |
| Spring Boot | 2.7.18 | 后端框架 | 提供完整的MVC架构和文件上传支持 |
| RESTful API | - | 前后端通信 | 提供标准化的API接口,支持文件上传下载 |
| JavaScript Blob | - | 前端文件处理 | 支持在浏览器中处理二进制文件,实现文件下载 |
API接口设计:
/api/contacts/export核心逻辑实现:
@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('导出联系人失败,请稍后重试');
}
}
API接口设计:
/api/contacts/import核心逻辑实现:
@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('导入联系人失败,请检查文件格式并稍后重试');
}
}
+----------------+ +----------------+ +----------------+
| 前端页面 | | RESTful API | | 后端服务 |
| (HTML/CSS/JS) |<----->| (Spring MVC) |<----->| (Spring Boot) |
+----------------+ +----------------+ +----------------+
|
v
+----------------+
| 数据库 |
| (H2/MySQL) |
+----------------+
首页/联系人列表页:

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

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

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

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


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

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

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

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

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

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

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

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

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

项目的全部内容为独立完成:
| 成员 | 贡献领域 | 贡献占比 | 贡献说明 |
|---|---|---|---|
| 李炳言 | 前端开发、后端开发、导入导出、收藏功能实现 | 100% | 独立完成前端核心页面开发,完成数据库设计与后端接口,实现导入导出功能,解决收藏状态同步问题,提交次数3次 |
| 任务类别 | 计划耗时(分钟) | 实际耗时(分钟) | 偏差原因分析 |
|---|---|---|---|
| 需求分析(功能拆解) | 60 | 80 | 对“多联系方式”的边界场景考虑不足,额外花费时间梳理需求 |
| 技术选型与设计 | 90 | 90 | 提前调研技术方案,设计过程顺利 |
| 前端开发(收藏功能等) | 240 | 300 | 遇到收藏状态同步问题,调试耗时较长 |
| Git提交与协作管理 | 30 | 40 | 分支合并冲突处理花费额外时间 |
| 数据库设计 | 60 | 180 | 前端后端接口对不上,反复调试检查代码花费大量时间 |
| 后端接口开发(导入导出) | 180 | 210 | Excel解析格式兼容问题调试耗时 |
| 功能测试与BUG修复 | 60 | 80 | 发现导入数据重复问题,需添加去重逻辑 |
| 文档撰写(博客/作业) | 60 | 60 | 按模板整理内容,效率较高 |
| 总计 | 780 | 1040 | 主要偏差来自数据库连接、功能调试与冲突处理 |