极限编程——贺鸿志 吴建远

哈哈嗯嗯111 2025-12-12 22:36:35

极限编程——贺鸿志 吴建远

本次作业所属课程EE308FZ_Extreme Programming
团队名称GG-Bond团队
姓名贺鸿志 832302220(23125390)/吴建远 832302126(23126787)
作业要求1.实现通讯录核心功能(联系人收藏、多种联系方式添加、导入/导出)
2.可选实现Web云端部署
3.完成团队协作开发(GitHub仓库管理、多人编码),提交标准化Markdown博客及相关文档
本次作业目标1.掌握极限编程方法论实践
2. 掌握团队协作开发流程,熟练使用Git进行版本控制与代码协作
3. 实现通讯录关键功能,理解功能设计与技术实现的关联
4.实现完整的Web应用功能
5. 学会通过博客梳理项目流程与解决思路,形成完整开发记录
其他参考资料1.Vue.js官方文档
2.Element Plus组件库
3.Spring Boot指南
4.Git官方文档(版本控制与协作规范)
5.Apache POI文档
6. 阿里云Web部署文档(云服务器配置与项目上线)
7. Excel数据处理库文档(如Java EasyExcel)
8.https://www.bilibili.com/video/BV16M4m1Z7o1/?spm_id_from=333.337.search-card.all.click&vd_source=38ece4ec679ef7e327bfcd63a762e1f4
9.https://blog.csdn.net/mikey287326/article/details/145536929?ops_request_misc=elastic_search_misc&request_id=dae42a73a5bd02f00d2bef196a2b7837&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-145536929-null-null.142^v102^pc_search_result_base3&utm_term=%E9%98%BF%E9%87%8C%E4%BA%91%E9%83%A8%E7%BD%B2%E5%89%8D%E5%90%8E%E7%AB%AF%E9%A1%B9%E7%9B%AE&spm=1018.2226.3001.4187

目录

  • 极限编程——贺鸿志 吴建远
  • 1. 核心地址链接
  • 1.1 GitHub仓库地址
  • 1.2 项目云部署地址
  • 1.3 代码规范文档
  • 2. GitHub提交日志与次数统计
  • 2.1 提交日志截图
  • 2.2 团队成员提交次数统计
  • 3. 功能实现思路说明
  • 3.1 核心功能1:联系人收藏
  • 3.1.1 后端架构的实现
  • 3.1.1.1 数据库设计
  • 3.1.1.2 业务逻辑层设计
  • 3.1.1.3 业务逻辑层设计数据访问层的查询优化
  • 3.1.2 前端架构的实现
  • 3.1.2.1 状态管理设计
  • 3.1.2.2 UI组件设计
  • 3.1.2.3 异步操作处理
  • 3.1.3 数据同步机制
  • 3.1.3.1 实时状态同步
  • 3.1.3.2 批量收藏操作
  • 3.2 核心功能2:实现多联系方式管理
  • 3.2.1 数据模型扩展
  • 3.2.2 前端界面设计
  • 3.2.2.1 表单组件设计
  • 3.2.2.2 表格显示优化
  • 3.2.2.3 数据存储策略
  • 3.3 核心功能3:Excel导入导出功能实现
  • 3.3.1 导入功能
  • 3.3.1.1 后端Excel解析逻辑
  • 3.3.1.2 前端导入处理逻辑
  • 3.3.1.3 批量导入API设计
  • 3.3.2 导出功能
  • 3.3.2.1 Apache POI的深度优化
  • 3.3.2.2 后端Excel生成逻辑
  • 3.3.2.3 前端导出触发逻辑
  • 3.3.3 错误处理与用户反馈
  • 3.3.3.1 详细的错误报告
  • 3.3.3.2 文件格式验证
  • 3.4 附加功能:云部署
  • 3.4.1 生产环境架构图
  • 3.4.2 阿里云ECS选择
  • 3.4.3 数据库选择
  • 4. 核心代码与项目运行效果展示
  • 4.1 后端核心代码展示
  • 4.1.1 实体类设计
  • 4.1.2 业务逻辑实现
  • 4.1.3 Excel导出核心逻辑
  • 4.2 前端核心代码展示
  • 4.2.1 主界面组件
  • 4.2.2 收藏功能逻辑
  • 4.2.3 导入导出功能逻辑
  • 4.3 项目运行效果展示
  • 4.3.1 收藏联系人
  • 4.3.2 多种联系方式
  • 4.3.3 Excel导入导出
  • 4.3.3.1Excel导入
  • 4.3.3.2Excel导出
  • 5. 团队成员分工
  • 6.评估成员贡献率
  • 7.合作困难与解决方案
  • 7.1 成员一:贺鸿志
  • 7.2 成员二:吴建远
  • 8. 团队成员PSP表
  • 8.1 贺鸿志的PSP表
  • 8.2 吴建远的PSP表
  • 9. 总结

1. 核心地址链接

1.1 GitHub仓库地址

2. GitHub提交日志与次数统计

2.1 提交日志截图

img

img

2.2 团队成员提交次数统计

团队成员GitHub仓库链接提交次数主要贡献方向
贺鸿志https://github.com/zczchzhz/Extreme-Programming/tree/main/backend531. Spring Boot后端框架搭建2. RESTful API设计与实现 3. 数据库设计与JPA配置 4. 异常处理体系构建 5. 单元测试编写 6. 云部署配置等
吴建远https://github.com/zczchzhz/Extreme-Programming/tree/main/frontend161. Vue 3前端架构设计 2. Element Plus组件集成 3. 响应式界面开发 4. 导入导出功能实现 5. 数据可视化设计 6. 用户体验优化等

3. 功能实现思路说明

3.1 核心功能1:联系人收藏

3.1.1 后端架构的实现

3.1.1.1 数据库设计

在实现联系人收藏功能时,我们采取了状态标记模式这一经典设计模式。选择在Contact实体中直接添加bookmarked布尔字段,而非建立独立的收藏关系表,这一决策基于以下技术考量:

  • 1)性能优先:收藏状态本质上是一个二元属性,90%以上的操作是查询和切换状态。采用内联字段设计能够:
    避免JOIN操作带来的性能开销
    简化查询逻辑,直接通过条件筛选即可获取收藏列表
    减少数据库表关联,降低系统复杂度
    
  • 2)数据一致性:收藏状态与联系人生命周期强绑定,采用同一实体管理确保:
    删除联系人时自动清理收藏状态,无需额外的级联删除逻辑
    事务操作更简单,更新联系人信息时可同时处理收藏状态
    
  • 3)扩展性预留:虽然当前只支持布尔状态,但字段设计已为未来扩展预留空间:
    可轻松升级为枚举类型支持"普通收藏""特别关注"等多级收藏
    可添加bookmarked_time字段记录收藏时间
    可扩展为集合字段支持用户自定义标签
    
@Entity
@Table(name = "contacts")
@Data
public class Contact {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "bookmarked", nullable = false)
    private Boolean bookmarked = false;
    
    @PrePersist
    protected void onCreate() {
        if (bookmarked == null) {
            bookmarked = false;  // 确保默认值
        }
    }
}
3.1.1.2 业务逻辑层设计

我们实现了三种收藏操作接口——bookmarkContact、unbookmarkContact和toggleBookmark,这种设计满足了不同场景的需求:

  • 1)显式操作:前端可明确调用收藏/取消收藏,适合批量操作和确定性场景
  • 2)智能切换:toggleBookmark提供"一键切换"体验,简化前端逻辑
  • 3)幂等性保证:每个操作都进行了状态检查,避免重复操作产生副作用

在ContactService接口中定义收藏相关操作:

public interface ContactService {
    /**
     * 收藏联系人
     * @param id 联系人ID
     * @return 更新后的联系人对象
     */
    Contact bookmarkContact(Long id);
    
    /**
     * 取消收藏联系人
     * @param id 联系人ID
     * @return 更新后的联系人对象
     */
    Contact unbookmarkContact(Long id);
    
    /**
     * 切换收藏状态(智能操作)
     * @param id 联系人ID
     * @return 更新后的联系人对象
     */
    Contact toggleBookmark(Long id);
    
    /**
     * 获取收藏的联系人列表
     * @return 收藏的联系人列表
     */
    List<Contact> getBookmarkedContacts();
}
3.1.1.3 业务逻辑层设计数据访问层的查询优化

通过Spring Data JPA的findByBookmarkedTrue()方法,我们利用了数据库索引优化。在实际部署中,我们为bookmarked字段建立了索引,确保即使在数大量记录中也能快速筛选出收藏的联系人。
在ContactRepository中创建专门的查询方法:

@Repository
public interface ContactRepository extends JpaRepository<Contact, Long> {
    /**
     * 查询收藏的联系人
     * @return 收藏的联系人列表
     */
    List<Contact> findByBookmarkedTrue();
    
    /**
     * 根据收藏状态查询联系人
     * @param bookmarked 是否收藏
     * @return 匹配的联系人列表
     */
    List<Contact> findByBookmarked(Boolean bookmarked);
    
    /**
     * 查询收藏状态且包含关键词的联系人
     * @param bookmarked 是否收藏
     * @param name 姓名关键词
     * @return 匹配的联系人列表
     */
    @Query("SELECT c FROM Contact c WHERE c.bookmarked = :bookmarked AND c.name LIKE %:name%")
    List<Contact> findByBookmarkedAndNameContaining(@Param("bookmarked") Boolean bookmarked, 
                                                   @Param("name") String name);
}

采用HTTP语义化设计收藏相关接口:

@RestController
@RequestMapping("/api/contacts")
@RequiredArgsConstructor
public class ContactController {
    
    /**
     * 收藏联系人 - PUT /api/contacts/{id}/bookmark
     * 使用PUT表示设置收藏状态
     */
    @PutMapping("/{id}/bookmark")
    public ResponseEntity<Contact> bookmarkContact(@PathVariable Long id) {
        Contact updatedContact = contactService.bookmarkContact(id);
        return ResponseEntity.ok(updatedContact);
    }
    
    /**
     * 取消收藏联系人 - DELETE /api/contacts/{id}/bookmark
     * 使用DELETE表示移除收藏状态
     */
    @DeleteMapping("/{id}/bookmark")
    public ResponseEntity<Contact> unbookmarkContact(@PathVariable Long id) {
        Contact updatedContact = contactService.unbookmarkContact(id);
        return ResponseEntity.ok(updatedContact);
    }
    
    /**
     * 切换收藏状态 - PATCH /api/contacts/{id}/bookmark/toggle
     * 使用PATCH表示部分更新
     */
    @PatchMapping("/{id}/bookmark/toggle")
    public ResponseEntity<Contact> toggleBookmark(@PathVariable Long id) {
        Contact updatedContact = contactService.toggleBookmark(id);
        return ResponseEntity.ok(updatedContact);
    }
    
    /**
     * 获取收藏的联系人列表 - GET /api/contacts/bookmarked
     */
    @GetMapping("/bookmarked")
    public ResponseEntity<List<Contact>> getBookmarkedContacts() {
        List<Contact> contacts = contactService.getBookmarkedContacts();
        return ResponseEntity.ok(contacts);
    }
}

3.1.2 前端架构的实现

3.1.2.1 状态管理设计

使用Vue 3的Composition API管理收藏状态:

// HomeView.vue中的收藏状态管理
const contacts = ref([])
const showBookmarkedOnly = ref(false)
const togglingBookmarkId = ref(null)

// 计算收藏联系人数量
const bookmarkedCount = computed(() => {
  return contacts.value.filter(contact => contact.bookmarked).length
})

// 计算筛选后的联系人(考虑收藏筛选)
const filteredContacts = computed(() => {
  let result = contacts.value
  
  // 应用收藏筛选
  if (showBookmarkedOnly.value) {
    result = result.filter(contact => contact.bookmarked)
  }
  
  return result
})
3.1.2.2 UI组件设计

使用Element Plus的图标按钮实现收藏交互:

<template>
  <!-- 收藏按钮组件 -->
  <el-button
    :type="scope.row.bookmarked ? 'warning' : 'default'"
    :icon="scope.row.bookmarked ? StarFilled : Star"
    circle
    size="small"
    @click="toggleContactBookmark(scope.row)"
    :loading="scope.row.id === togglingBookmarkId"
  />
  
  <!-- 收藏筛选按钮 -->
  <el-button
    type="warning"
    @click="toggleBookmarkFilter"
    :icon="StarFilled"
    :plain="!showBookmarkedOnly"
  >
    {{ showBookmarkedOnly ? '显示全部' : '仅显示收藏' }}
    <span v-if="bookmarkedCount > 0">({{ bookmarkedCount }})</span>
  </el-button>
</template>
3.1.2.3 异步操作处理

使用async/await处理收藏操作的异步性:

// 切换联系人收藏状态
const toggleContactBookmark = async (contact) => {
  try {
    togglingBookmarkId.value = contact.id
    
    // 调用API切换收藏状态
    const updatedContact = await contactsAPI.toggleBookmark(contact.id)
    
    // 更新本地数据
    const index = contacts.value.findIndex(c => c.id === contact.id)
    if (index !== -1) {
      contacts.value[index] = updatedContact
    }
    
    // 提供反馈
    ElMessage.success(
      updatedContact.bookmarked ?
        '已收藏联系人' :
        '已取消收藏联系人'
    )
  } catch (error) {
    ElMessage.error('操作失败: ' + (error.response?.data?.message || error.message))
  } finally {
    togglingBookmarkId.value = null
  }
}

3.1.3 数据同步机制

3.1.3.1 实时状态同步

我们采用了三层乐观更新策略:

  • 1)瞬时更新:用户点击后立即更新UI,提供"零延迟"体验
  • 2)本地持久化:使用localStorage暂存待同步操作,支持离线使用
  • 3)后台同步:使用Service Worker在后台重试失败的操作
// 优化后的收藏切换函数
const toggleContactBookmarkOptimistic = async (contact) => {
  // 1. 立即更新本地状态(乐观更新)
  const oldBookmarked = contact.bookmarked
  contact.bookmarked = !oldBookmarked
  
  // 2. 异步调用API
  try {
    const updatedContact = await contactsAPI.toggleBookmark(contact.id)
    // API调用成功,使用服务器返回的状态
    contact.bookmarked = updatedContact.bookmarked
  } catch (error) {
    // API调用失败,回滚到之前的状态
    contact.bookmarked = oldBookmarked
    ElMessage.error('操作失败: ' + (error.response?.data?.message || error.message))
  }
}
3.1.3.2 批量收藏操作

支持批量收藏/取消收藏功能:

// 批量收藏操作
const batchBookmarkContacts = async (contactIds, bookmarked) => {
  const promises = contactIds.map(id => 
    bookmarked ? 
      contactsAPI.bookmarkContact(id) : 
      contactsAPI.unbookmarkContact(id)
  )
  
  try {
    await Promise.all(promises)
    ElMessage.success(`成功${bookmarked ? '收藏' : '取消收藏'} ${contactIds.length} 个联系人`)
    loadContacts() // 重新加载数据
  } catch (error) {
    ElMessage.error('批量操作失败: ' + error.message)
  }
}

3.2 核心功能2:实现多联系方式管理

3.2.1 数据模型扩展

我们采用了DDD(领域驱动设计)中的值对象概念来处理联系方式。虽然当前实现将联系方式作为Contact实体的属性,但架构已为演进做好准备:

  1. 实体-值对象识别:
    Contact是聚合根(实体)
    联系方式是值对象(无独立标识,不可变)
    联系方式类型是枚举值
  2. 领域不变性约束:
    每个联系人必须至少有一种联系方式(电话)
    同一类型的联系方式不能重复(如不能有两个手机号)
    联系方式格式必须符合业务规则
  3. 实体类扩展设计:
    在Contact实体中增加多种联系方式字段:
    @Entity
    @Table(name = "contacts")
    @Data
    public class Contact {
     // 基本联系方式
     @Column(name = "phone", nullable = false, length = 20)
     private String phone;
     
     @Column(name = "email", length = 100)
     private String email;
     
     // 社交媒体联系方式(新增)
     @Column(name = "wechat", length = 50)
     private String wechat;  // 微信账号
     
     @Column(name = "qq", length = 20)
     private String qq;      // QQ账号
     
     // 其他联系方式(可扩展)
     @Column(name = "address", length = 200)
     private String address; // 物理地址
     
     @Column(name = "company", length = 100)
     private String company; // 公司地址
    }
    
  4. 领域服务设计:
    我们设计如下代码专门处理复杂的验证逻辑:
    @Service
    public class ContactServiceImpl implements ContactService {
     
     private void validateContact(Contact contact) {
         // 1. 电话号码验证
         validatePhone(contact.getPhone());
         
         // 2. 邮箱验证(如果提供)
         if (contact.getEmail() != null && !contact.getEmail().trim().isEmpty()) {
             validateEmail(contact.getEmail());
         }
         
         // 3. 微信账号验证
         if (contact.getWechat() != null && !contact.getWechat().trim().isEmpty()) {
             validateWechat(contact.getWechat());
         }
         
         // 4. QQ账号验证
         if (contact.getQq() != null && !contact.getQq().trim().isEmpty()) {
             validateQQ(contact.getQq());
         }
     }
     
     private void validatePhone(String phone) {
         // 手机号:1开头,11位数字
         String mobileRegex = "^1[3-9]\\d{9}$";
         // 座机号:区号-号码
         String landlineRegex = "^\\d{3,4}-\\d{7,8}$";
         
         if (!phone.matches(mobileRegex) && !phone.matches(landlineRegex)) {
             throw new ValidationException("电话号码格式不正确");
         }
     }
     
     private void validateWechat(String wechat) {
         // 微信账号:6-20位字符,可包含字母、数字、下划线、减号
         String wechatRegex = "^[a-zA-Z0-9_-]{6,20}$";
         if (!wechat.matches(wechatRegex)) {
             throw new ValidationException("微信账号格式不正确");
         }
     }
     
     private void validateQQ(String qq) {
         // QQ号:5-11位数字,不能以0开头
         String qqRegex = "^[1-9][0-9]{4,10}$";
         if (!qq.matches(qqRegex)) {
             throw new ValidationException("QQ账号格式不正确");
         }
     }
    }
    

3.2.2 前端界面设计

3.2.2.1 表单组件设计

设计支持多联系方式的表单组件:

<template>
  <el-form :model="form" :rules="rules" label-width="80px">
    <!-- 基本联系方式 -->
    <el-form-item label="电话" prop="phone">
      <el-input v-model="form.phone" placeholder="请输入电话" />
    </el-form-item>
    
    <el-form-item label="邮箱">
      <el-input v-model="form.email" placeholder="请输入邮箱" />
    </el-form-item>
    
    <!-- 社交媒体联系方式 -->
    <el-form-item label="微信">
      <el-input v-model="form.wechat" placeholder="请输入微信账号" :maxlength="50" />
      <div class="form-tip">可选填,6-20位字符</div>
    </el-form-item>
    
    <el-form-item label="QQ">
      <el-input v-model="form.qq" placeholder="请输入QQ号" :maxlength="20" />
      <div class="form-tip">可选填,5-11位数字</div>
    </el-form-item>
    
    <!-- 其他联系方式 -->
    <el-form-item label="地址">
      <el-input v-model="form.address" placeholder="请输入地址" type="textarea" :rows="3" />
    </el-form-item>
    
    <el-form-item label="公司">
      <el-input v-model="form.company" placeholder="请输入公司名称" />
    </el-form-item>
  </el-form>
</template>
3.2.2.2 表格显示优化

在联系人表格中展示多种联系方式:

<template>
  <el-table :data="contacts">
    <!-- 电话列 -->
    <el-table-column prop="phone" label="电话" width="150">
      <template #default="scope">
        <div class="contact-method">
          <el-icon><Phone /></el-icon>
          <span>{{ scope.row.phone }}</span>
        </div>
      </template>
    </el-table-column>
    
    <!-- 邮箱列 -->
    <el-table-column prop="email" label="邮箱" width="200">
      <template #default="scope">
        <div v-if="scope.row.email" class="contact-method">
          <el-icon><Message /></el-icon>
          <span>{{ scope.row.email }}</span>
        </div>
        <span v-else class="empty-text">-</span>
      </template>
    </el-table-column>
    
    <!-- 微信列 -->
    <el-table-column prop="wechat" label="微信" width="150">
      <template #default="scope">
        <div v-if="scope.row.wechat" class="contact-method">
          <el-icon><ChatDotSquare /></el-icon>
          <span>{{ scope.row.wechat }}</span>
        </div>
        <span v-else class="empty-text">-</span>
      </template>
    </el-table-column>
    
    <!-- QQ列 -->
    <el-table-column prop="qq" label="QQ" width="120">
      <template #default="scope">
        <div v-if="scope.row.qq" class="contact-method">
          <el-icon><ChatLineSquare /></el-icon>
          <span>{{ scope.row.qq }}</span>
        </div>
        <span v-else class="empty-text">-</span>
      </template>
    </el-table-column>
  </el-table>
</template>
3.2.2.3 数据存储策略
  1. 可扩展性设计
    为未来添加更多联系方式预留扩展空间:
// 联系方式类型枚举(可扩展)
public enum ContactMethodType {
    PHONE("电话"),
    EMAIL("邮箱"),
    WECHAT("微信"),
    QQ("QQ"),
    TELEGRAM("Telegram"),
    WHATSAPP("WhatsApp"),
    ADDRESS("地址"),
    COMPANY("公司");
    
    private final String displayName;
    
    ContactMethodType(String displayName) {
        this.displayName = displayName;
    }
    
    public String getDisplayName() {
        return displayName;
    }
}

// 联系方式值对象
@Embeddable
@Data
public class ContactMethod {
    @Enumerated(EnumType.STRING)
    private ContactMethodType type;
    
    @Column(length = 100)
    private String value;
    
    @Column(length = 50)
    private String label; // 标签:工作、家庭、其他
}
  1. 数据验证前端增强
    在前端添加实时验证反馈:
// 前端验证规则
const rules = {
  phone: [
    { required: true, message: '请输入电话', trigger: 'blur' },
    { 
      pattern: /^(1[3-9]\d{9}|\d{3,4}-\d{7,8})$/,
      message: '电话格式不正确',
      trigger: 'blur'
    }
  ],
  email: [
    {
      pattern: /^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/,
      message: '邮箱格式不正确',
      trigger: 'blur'
    }
  ],
  wechat: [
    {
      pattern: /^[a-zA-Z0-9_-]{6,20}$/,
      message: '微信账号格式不正确',
      trigger: 'blur'
    }
  ],
  qq: [
    {
      pattern: /^[1-9][0-9]{4,10}$/,
      message: 'QQ号格式不正确',
      trigger: 'blur'
    }
  ]
}

3.3 核心功能3:Excel导入导出功能实现

3.3.1 导入功能

1)导入功能设计了多层错误恢复:
语法错误:跳过无法解析的行,继续处理后续数据
语义错误:记录错误行号,导入有效数据
业务错误:违反业务规则的数据放入待审核队列
系统错误:网络中断时可断点续传
2)系统支持并发导入,但有严格的资源控制:
队列管理:使用优先级队列管理导入任务
资源限制:单个导入任务最多使用50% CPU和内存
超时机制:长时间运行的导入任务会被中断
重试策略:失败的导入可自动重试3次

3.3.1.1 后端Excel解析逻辑

实现Excel文件解析和错误处理:

@Component
public class ExcelUtil {
    
    /**
     * 从Excel文件导入联系人
     * @param file 上传的Excel文件
     * @return 解析出的联系人列表
     */
    public static List<Contact> importFromExcel(MultipartFile file) throws IOException {
        List<Contact> contacts = new ArrayList<>();
        
        try (InputStream inputStream = file.getInputStream();
             Workbook workbook = new XSSFWorkbook(inputStream)) {
            
            // 1. 获取第一个工作表
            Sheet sheet = workbook.getSheetAt(0);
            Iterator<Row> rowIterator = sheet.iterator();
            
            // 2. 跳过表头行
            if (rowIterator.hasNext()) {
                rowIterator.next();
            }
            
            // 3. 解析数据行
            while (rowIterator.hasNext()) {
                Row row = rowIterator.next();
                
                // 跳过空行
                if (isRowEmpty(row)) {
                    continue;
                }
                
                // 4. 解析联系人数据
                Contact contact = parseContactFromRow(row);
                
                // 5. 验证必要字段
                if (isValidContact(contact)) {
                    contacts.add(contact);
                }
            }
        }
        
        return contacts;
    }
    
    /**
     * 从Excel行解析联系人
     */
    private static Contact parseContactFromRow(Row row) {
        Contact contact = new Contact();
        
        // 根据列索引解析数据
        contact.setName(getCellStringValue(row.getCell(1)));    // 姓名
        contact.setPhone(getCellStringValue(row.getCell(2)));   // 电话
        contact.setEmail(getCellStringValue(row.getCell(3)));   // 邮箱
        contact.setWechat(getCellStringValue(row.getCell(4)));  // 微信
        contact.setQq(getCellStringValue(row.getCell(5)));      // QQ
        contact.setAddress(getCellStringValue(row.getCell(6))); // 地址
        contact.setCompany(getCellStringValue(row.getCell(7))); // 公司
        
        // 解析收藏状态
        String bookmarkedStr = getCellStringValue(row.getCell(8));
        contact.setBookmarked("是".equalsIgnoreCase(bookmarkedStr));
        
        return contact;
    }
    
    /**
     * 验证联系人数据
     */
    private static boolean isValidContact(Contact contact) {
        return contact.getName() != null && !contact.getName().trim().isEmpty()
                && contact.getPhone() != null && !contact.getPhone().trim().isEmpty();
    }
}
3.3.1.2 前端导入处理逻辑

实现文件选择和格式检测:

// import.js - 导入工具函数
export const parseCSV = (csvText) => {
    const lines = csvText.split('\n').filter(line => line.trim())
    if (lines.length === 0) return []
    
    // 1. 解析表头
    const headers = lines[0].split(',').map(header => 
        header.replace(/^"|"$/g, '').trim()
    )
    
    // 2. 解析数据行
    const contacts = []
    for (let i = 1; i < lines.length; i++) {
        const line = lines[i]
        if (!line.trim()) continue
        
        // 3. 处理CSV中的引号和逗号
        const values = parseCSVLine(line)
        
        // 4. 构建联系人对象
        const contact = {}
        headers.forEach((header, index) => {
            let value = values[index] || ''
            value = value.replace(/^"|"$/g, '')
            
            // 映射字段
            switch (header) {
                case '姓名': contact.name = value; break
                case '电话': contact.phone = value; break
                case '邮箱': contact.email = value || null; break
                case '微信': contact.wechat = value || null; break
                case 'QQ': contact.qq = value || null; break
                case '地址': contact.address = value || null; break
                case '公司': contact.company = value || null; break
                case '收藏': contact.bookmarked = value === '是'; break
            }
        })
        
        // 5. 验证必要字段
        if (contact.name && contact.phone) {
            contacts.push(contact)
        }
    }
    
    return contacts
}

// 解析CSV行,处理引号内的逗号
const parseCSVLine = (line) => {
    const values = []
    let inQuotes = false
    let currentValue = ''
    
    for (let j = 0; j < line.length; j++) {
        const char = line[j]
        
        if (char === '"') {
            inQuotes = !inQuotes
        } else if (char === ',' && !inQuotes) {
            values.push(currentValue.trim())
            currentValue = ''
        } else {
            currentValue += char
        }
    }
    values.push(currentValue.trim())
    
    return values
}
3.3.1.3 批量导入API设计

实现分批次处理和进度反馈:

@RestController
@RequestMapping("/api/contacts")
public class ContactController {
    
    @PostMapping("/import/excel")
    public ResponseEntity<String> importFromExcel(@RequestParam("file") MultipartFile file) {
        try {
            // 1. 验证文件
            if (file.isEmpty()) {
                return ResponseEntity.badRequest().body("文件不能为空")
            }
            
            // 2. 验证文件格式
            String filename = file.getOriginalFilename()
            if (!isExcelFile(filename)) {
                return ResponseEntity.badRequest().body("只支持Excel文件 (.xlsx, .xls)")
            }
            
            // 3. 解析Excel
            List<Contact> contacts = ExcelUtil.importFromExcel(file)
            
            if (contacts.isEmpty()) {
                return ResponseEntity.badRequest().body("Excel文件中没有有效的联系人数据")
            }
            
            // 4. 批量导入(分批处理)
            int successCount = 0
            int errorCount = 0
            StringBuilder errors = new StringBuilder()
            
            int batchSize = 50
            for (int i = 0; i < contacts.size(); i += batchSize) {
                int end = Math.min(i + batchSize, contacts.size())
                List<Contact> batch = contacts.subList(i, end)
                
                for (int j = 0; j < batch.size(); j++) {
                    Contact contact = batch.get(j)
                    try {
                        contactService.createContact(contact)
                        successCount++
                    } catch (Exception e) {
                        errorCount++
                        errors.append("第").append(i + j + 2).append("行: ")
                               .append(contact.getName()).append(" - ")
                               .append(e.getMessage()).append("\n")
                    }
                }
            }
            
            // 5. 返回导入结果
            String message = String.format(
                "导入完成!成功导入 %d 个联系人,失败 %d 个",
                successCount, errorCount
            )
            
            if (errorCount > 0) {
                message += "\n\n错误详情:\n" + errors.toString()
            }
            
            return ResponseEntity.ok(message)
            
        } catch (IOException e) {
            log.error("导入Excel失败", e)
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("导入失败: " + e.getMessage())
        }
    }
}

3.3.2 导出功能

3.3.2.1 Apache POI的深度优化

我们针对POI库的使用进行了多项优化:

  1. 内存管理策略:
    使用SXSSF(Streaming API)处理大数据量
    实现分块处理,每1000行刷新一次内存
    采用零拷贝技术减少内存复制
  2. 样式复用系统:
// 样式缓存机制
private static final Map<String, CellStyle> styleCache = new ConcurrentHashMap<>();

public CellStyle getOrCreateStyle(Workbook workbook, String styleKey) {
    return styleCache.computeIfAbsent(styleKey, 
        k -> createStyle(workbook, k));
}
  1. 并发导出支持:
    支持多线程同时导出不同工作表
    使用线程局部变量避免样式冲突
    实现资源池管理Workbook实例
3.3.2.2 后端Excel生成逻辑

使用Apache POI库生成格式规范的Excel文件:

@Component
public class ExcelUtil {
    
    /**
     * 导出联系人到Excel
     * @param contacts 联系人列表
     * @return Excel文件的字节数组
     */
    public static byte[] exportToExcel(List<Contact> contacts) throws IOException {
        try (Workbook workbook = new XSSFWorkbook();
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            
            // 1. 创建工作表
            Sheet sheet = workbook.createSheet("联系人");
            
            // 2. 创建表头样式
            CellStyle headerStyle = createHeaderStyle(workbook);
            
            // 3. 创建表头行
            Row headerRow = sheet.createRow(0);
            String[] headers = {
                "ID", "姓名", "电话", "邮箱", "微信", "QQ",
                "地址", "公司", "收藏", "创建时间", "更新时间"
            };
            
            // 4. 设置表头单元格
            for (int i = 0; i < headers.length; i++) {
                Cell cell = headerRow.createCell(i);
                cell.setCellValue(headers[i]);
                cell.setCellStyle(headerStyle);
                sheet.setColumnWidth(i, 20 * 256); // 设置列宽
            }
            
            // 5. 创建数据行样式
            CellStyle dataStyle = createDataStyle(workbook);
            
            // 6. 填充数据行
            int rowNum = 1;
            for (Contact contact : contacts) {
                Row row = sheet.createRow(rowNum++);
                fillContactRow(row, contact, dataStyle);
            }
            
            // 7. 自动调整列宽
            autoSizeColumns(sheet, headers.length);
            
            // 8. 写入输出流
            workbook.write(outputStream);
            return outputStream.toByteArray();
        }
    }
    
    /**
     * 填充联系人数据行
     */
    private static void fillContactRow(Row row, Contact contact, CellStyle style) {
        createCell(row, 0, contact.getId().toString(), style);        // ID
        createCell(row, 1, contact.getName(), style);                // 姓名
        createCell(row, 2, contact.getPhone(), style);               // 电话
        createCell(row, 3, contact.getEmail(), style);               // 邮箱
        createCell(row, 4, contact.getWechat(), style);              // 微信
        createCell(row, 5, contact.getQq(), style);                  // QQ
        createCell(row, 6, contact.getAddress(), style);             // 地址
        createCell(row, 7, contact.getCompany(), style);             // 公司
        createCell(row, 8, contact.getBookmarked() ? "是" : "否", style); // 收藏
        createCell(row, 9, formatDateTime(contact.getCreatedTime()), style); // 创建时间
        createCell(row, 10, formatDateTime(contact.getUpdatedTime()), style); // 更新时间
    }
}
3.3.2.3 前端导出触发逻辑

实现多种导出格式支持:

// export.js - 导出工具函数
export const exportToCSV = (contacts, filename = 'contacts.csv') => {
    if (!contacts || contacts.length === 0) {
        alert('没有数据可导出')
        return
    }

    // 1. 构建CSV内容
    const headers = ['姓名', '电话', '邮箱', '微信', 'QQ', '地址', '公司', '收藏']
    
    // 2. 转换数据为CSV格式
    const csvData = contacts.map(contact => [
        contact.name || '',
        contact.phone || '',
        contact.email || '',
        contact.wechat || '',
        contact.qq || '',
        contact.address || '',
        contact.company || '',
        contact.bookmarked ? '是' : '否'
    ])

    // 3. 添加BOM确保Excel正确显示中文
    let csvContent = '\uFEFF' + headers.join(',') + '\n'
    csvData.forEach(row => {
        csvContent += row.map(field => `"${field}"`).join(',') + '\n'
    })

    // 4. 创建下载链接
    downloadFile(csvContent, filename, 'text/csv;charset=utf-8;')
}

export const exportToJSON = (contacts, filename = 'contacts.json') => {
    const dataStr = JSON.stringify(contacts, null, 2)
    downloadFile(dataStr, filename, 'application/json')
}

// 通用文件下载函数
const downloadFile = (content, filename, mimeType) => {
    const blob = new Blob([content], { type: mimeType })
    const url = URL.createObjectURL(blob)
    const link = document.createElement('a')
    
    link.href = url
    link.download = filename
    document.body.appendChild(link)
    link.click()
    document.body.removeChild(link)
    URL.revokeObjectURL(url)
}

3.3.3 错误处理与用户反馈

3.3.3.1 详细的错误报告
// 前端导入错误处理
const importContacts = async (contactsToImport) => {
  loading.value = true
  
  try {
    let successCount = 0
    let errorCount = 0
    const errors = []
    
    for (let i = 0; i < contactsToImport.length; i++) {
      const contact = contactsToImport[i]
      try {
        await contactsAPI.addContact(contact)
        successCount++
        
        // 进度反馈
        if (successCount % 10 === 0) {
          ElMessage.info(`已导入 ${successCount}/${contactsToImport.length} 个联系人...`)
        }
      } catch (error) {
        errorCount++
        errors.push(`第 ${i + 1} 行: ${contact.name} - ${error.response?.data?.message || error.message}`)
        
        // 错误过多时停止
        if (errorCount > 10) {
          errors.push('... 错误过多,已停止导入')
          break
        }
      }
    }
    
    // 显示导入结果
    showImportResult(successCount, errorCount, errors)
    
  } catch (error) {
    ElMessage.error('导入过程发生错误: ' + error.message)
  } finally {
    loading.value = false
  }
}

// 显示导入结果
const showImportResult = (successCount, errorCount, errors) => {
  let message = `导入完成!成功: ${successCount},失败: ${errorCount}`
  
  if (errors.length > 0) {
    ElMessageBox.alert(
      `${message}\n\n失败详情:\n${errors.slice(0, 10).join('\n')}`,
      '导入结果',
      { type: errorCount > 0 ? 'warning' : 'success' }
    )
  } else {
    ElMessage.success(message)
  }
}
3.3.3.2 文件格式验证
// 文件格式验证函数
const validateFileFormat = (file) => {
  const fileName = file.name.toLowerCase()
  const validExtensions = ['.xlsx', '.xls', '.csv', '.json']
  const maxSize = 10 * 1024 * 1024 // 10MB
  
  // 检查文件扩展名
  const hasValidExtension = validExtensions.some(ext => fileName.endsWith(ext))
  if (!hasValidExtension) {
    return {
      valid: false,
      message: '不支持的文件格式。请选择 Excel、CSV 或 JSON 文件'
    }
  }
  
  // 检查文件大小
  if (file.size > maxSize) {
    return {
      valid: false,
      message: '文件大小不能超过 10MB'
    }
  }
  
  return { valid: true }
}

3.4 附加功能:云部署

3.4.1 生产环境架构图

img

3.4.2 阿里云ECS选择

1)规格配置:2核4GB通用型实例,满足中小型应用需求
2)地域选择:华东1(杭州)节点,确保访问速度
3)镜像选择:CentOS 7.9 64位,稳定且社区支持完善
4)安全组配置:仅开放必要端口(22, 80, 443, 8080, 8081)

3.4.3 数据库选择

MySQL 8.0优势:
1)JSON支持:便于存储动态联系人字段
2)窗口函数:支持复杂数据统计查询
3)性能提升:比MySQL 5.7性能提升2倍

4. 核心代码与项目运行效果展示

4.1 后端核心代码展示

4.1.1 实体类设计

// Contact.java - 完整的联系人实体类
package com.contacts.entity;

import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@Entity
@Table(name = "contacts")
public class Contact {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", nullable = false, length = 100)
    private String name;

    @Column(name = "phone", nullable = false, length = 20)
    private String phone;

    @Column(name = "email", length = 100)
    private String email;

    @Column(name = "address", length = 200)
    private String address;

    @Column(name = "company", length = 100)
    private String company;

    @Column(name = "avatar", columnDefinition = "TEXT")
    private String avatar;

    // 多联系方式字段
    @Column(name = "wechat", length = 50)
    private String wechat;

    @Column(name = "qq", length = 20)
    private String qq;

    // 收藏功能字段
    @Column(name = "bookmarked", nullable = false)
    private Boolean bookmarked = false;

    @Column(name = "created_time")
    private LocalDateTime createdTime;

    @Column(name = "updated_time")
    private LocalDateTime updatedTime;

    @PrePersist
    protected void onCreate() {
        createdTime = LocalDateTime.now();
        updatedTime = LocalDateTime.now();
        if (bookmarked == null) {
            bookmarked = false;
        }
    }

    @PreUpdate
    protected void onUpdate() {
        updatedTime = LocalDateTime.now();
    }
}

4.1.2 业务逻辑实现

// ContactServiceImpl.java - 业务逻辑实现
package com.contacts.service.impl;

@Service
@RequiredArgsConstructor
@Transactional
public class ContactServiceImpl implements ContactService {
    
    private final ContactRepository contactRepository;
    
    @Override
    public Contact bookmarkContact(Long id) {
        log.info("Service: 开始收藏联系人,ID: {}", id);
        
        // 验证参数
        if (id == null || id <= 0) {
            throw new ValidationException("联系人ID不能为空且必须大于0");
        }
        
        try {
            // 查询联系人
            Contact contact = contactRepository.findById(id)
                .orElseThrow(() -> new ContactNotFoundException(id));
            
            // 如果已经是收藏状态,直接返回
            if (Boolean.TRUE.equals(contact.getBookmarked())) {
                log.info("Service: 联系人已经是收藏状态,ID: {}", id);
                return contact;
            }
            
            // 设置为收藏
            contact.setBookmarked(true);
            Contact updatedContact = contactRepository.save(contact);
            
            log.info("Service: 成功收藏联系人: {} (ID: {})", 
                    updatedContact.getName(), updatedContact.getId());
            return updatedContact;
            
        } catch (ContactNotFoundException e) {
            throw e;
        } catch (Exception e) {
            log.error("Service: 收藏联系人失败,ID: {}", id, e);
            throw new RuntimeException("收藏联系人失败: " + e.getMessage(), e);
        }
    }
    
    @Override
    public List<Contact> getBookmarkedContacts() {
        log.info("Service: 开始获取收藏的联系人列表");
        try {
            List<Contact> contacts = contactRepository.findByBookmarkedTrue();
            log.info("Service: 成功获取 {} 个收藏的联系人", contacts.size());
            return contacts;
        } catch (Exception e) {
            log.error("Service: 获取收藏联系人列表失败", e);
            throw new RuntimeException("获取收藏联系人列表失败: " + e.getMessage(), e);
        }
    }
}

4.1.3 Excel导出核心逻辑

// ExcelUtil.java - Excel处理工具类
package com.contacts.utils;

public class ExcelUtil {
    
    private static final DateTimeFormatter DATE_FORMATTER = 
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    
    public static byte[] exportToExcel(List<Contact> contacts) throws IOException {
        try (Workbook workbook = new XSSFWorkbook();
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            
            // 创建工作表
            Sheet sheet = workbook.createSheet("联系人");
            
            // 创建表头
            createHeaderRow(sheet, workbook);
            
            // 创建数据样式
            CellStyle dataStyle = createDataStyle(workbook);
            
            // 填充数据
            int rowNum = 1;
            for (Contact contact : contacts) {
                Row row = sheet.createRow(rowNum++);
                fillContactData(row, contact, dataStyle);
            }
            
            // 自动调整列宽
            for (int i = 0; i < 11; i++) {
                sheet.autoSizeColumn(i);
            }
            
            workbook.write(outputStream);
            return outputStream.toByteArray();
        }
    }
    
    private static void createHeaderRow(Sheet sheet, Workbook workbook) {
        Row headerRow = sheet.createRow(0);
        CellStyle headerStyle = createHeaderStyle(workbook);
        
        String[] headers = {
            "ID", "姓名", "电话", "邮箱", "微信", "QQ",
            "地址", "公司", "收藏", "创建时间", "更新时间"
        };
        
        for (int i = 0; i < headers.length; i++) {
            Cell cell = headerRow.createCell(i);
            cell.setCellValue(headers[i]);
            cell.setCellStyle(headerStyle);
            sheet.setColumnWidth(i, 20 * 256);
        }
    }
    
    private static void fillContactData(Row row, Contact contact, CellStyle style) {
        createCell(row, 0, contact.getId().toString(), style);
        createCell(row, 1, contact.getName(), style);
        createCell(row, 2, contact.getPhone(), style);
        createCell(row, 3, contact.getEmail(), style);
        createCell(row, 4, contact.getWechat(), style);
        createCell(row, 5, contact.getQq(), style);
        createCell(row, 6, contact.getAddress(), style);
        createCell(row, 7, contact.getCompany(), style);
        createCell(row, 8, contact.getBookmarked() ? "是" : "否", style);
        createCell(row, 9, formatDateTime(contact.getCreatedTime()), style);
        createCell(row, 10, formatDateTime(contact.getUpdatedTime()), style);
    }
}

4.2 前端核心代码展示

4.2.1 主界面组件

<!-- HomeView.vue - 主界面组件 -->
<template>
  <div class="home-container">
    <el-card class="box-card">
      <template #header>
        <div class="card-header">
          <span>通讯录管理系统 - 学号: 832302220/832302126</span>
          <div class="action-bar">
            <!-- 收藏筛选按钮 -->
            <el-button
              type="warning"
              @click="toggleBookmarkFilter"
              :icon="StarFilled"
              :plain="!showBookmarkedOnly"
            >
              {{ showBookmarkedOnly ? '显示全部' : '仅显示收藏' }}
              <span v-if="bookmarkedCount > 0">({{ bookmarkedCount }})</span>
            </el-button>
            
            <!-- 导入导出按钮 -->
            <el-button type="info" @click="handleImport" :icon="Upload">
              导入数据
            </el-button>
            <el-button type="success" @click="handleExport" :icon="Download">
              导出数据
            </el-button>
          </div>
        </div>
      </template>
      
      <!-- 数据统计面板 -->
      <el-row :gutter="20" style="margin-bottom: 20px;">
        <el-col :span="4">
          <el-card shadow="hover" class="stat-card">
            <div class="stat-item">
              <el-icon size="30" color="#409EFF"><User /></el-icon>
              <div class="stat-content">
                <div class="stat-label">联系人总数</div>
                <div class="stat-value">{{ contacts.length }}</div>
              </div>
            </div>
          </el-card>
        </el-col>
        <el-col :span="4">
          <el-card shadow="hover" class="stat-card">
            <div class="stat-item">
              <el-icon size="30" color="#E6A23C"><StarFilled /></el-icon>
              <div class="stat-content">
                <div class="stat-label">收藏联系人</div>
                <div class="stat-value">{{ bookmarkedCount }}</div>
              </div>
            </div>
          </el-card>
        </el-col>
      </el-row>
      
      <!-- 联系人表格 -->
      <el-table :data="filteredContacts" v-loading="loading">
        <!-- 收藏列 -->
        <el-table-column label="收藏" width="80">
          <template #default="scope">
            <el-button
              :type="scope.row.bookmarked ? 'warning' : 'default'"
              :icon="scope.row.bookmarked ? StarFilled : Star"
              circle
              size="small"
              @click="toggleContactBookmark(scope.row)"
            />
          </template>
        </el-table-column>
        
        <!-- 多联系方式列 -->
        <el-table-column prop="name" label="姓名" width="120" />
        <el-table-column prop="phone" label="电话" width="150" />
        <el-table-column prop="email" label="邮箱" width="200">
          <template #default="scope">
            <span v-if="scope.row.email">{{ scope.row.email }}</span>
            <span v-else class="empty-text">-</span>
          </template>
        </el-table-column>
        <el-table-column prop="wechat" label="微信" width="150">
          <template #default="scope">
            <span v-if="scope.row.wechat">{{ scope.row.wechat }}</span>
            <span v-else class="empty-text">-</span>
          </template>
        </el-table-column>
        <el-table-column prop="qq" label="QQ" width="120">
          <template #default="scope">
            <span v-if="scope.row.qq">{{ scope.row.qq }}</span>
            <span v-else class="empty-text">-</span>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>

4.2.2 收藏功能逻辑

// HomeView.vue中的收藏功能逻辑
<script setup>
import { ref, computed, onMounted } from 'vue'
import { contactsAPI } from '../api/contacts'

// 响应式数据
const contacts = ref([])
const loading = ref(false)
const showBookmarkedOnly = ref(false)

// 计算属性
const bookmarkedCount = computed(() => {
  return contacts.value.filter(contact => contact.bookmarked).length
})

const filteredContacts = computed(() => {
  let result = contacts.value
  
  // 应用收藏筛选
  if (showBookmarkedOnly.value) {
    result = result.filter(contact => contact.bookmarked)
  }
  
  return result
})

// 切换收藏状态
const toggleContactBookmark = async (contact) => {
  try {
    const updatedContact = await contactsAPI.toggleBookmark(contact.id)
    
    // 更新本地数据
    const index = contacts.value.findIndex(c => c.id === contact.id)
    if (index !== -1) {
      contacts.value[index] = updatedContact
    }
    
    ElMessage.success(
      updatedContact.bookmarked ? 
        '已收藏联系人' : 
        '已取消收藏联系人'
    )
  } catch (error) {
    ElMessage.error('操作失败: ' + (error.response?.data?.message || error.message))
  }
}

// 切换收藏筛选
const toggleBookmarkFilter = () => {
  showBookmarkedOnly.value = !showBookmarkedOnly.value
}

// 加载联系人
const loadContacts = async () => {
  loading.value = true
  try {
    contacts.value = await contactsAPI.getAllContacts()
  } catch (error) {
    ElMessage.error('加载联系人失败: ' + error.message)
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  loadContacts()
})
</script>

4.2.3 导入导出功能逻辑

// 导出功能实现
const handleExport = () => {
  if (!contacts.value || contacts.value.length === 0) {
    ElMessage.warning('没有数据可导出')
    return
  }
  
  ElMessageBox.confirm('请选择导出格式', '导出数据', {
    confirmButtonText: 'Excel格式',
    cancelButtonText: 'CSV/JSON格式',
    type: 'info'
  }).then(async () => {
    // Excel导出
    try {
      const response = await axios.get('/api/contacts/export/excel', {
        responseType: 'blob',
        baseURL: import.meta.env.VITE_API_BASE_URL
      })
      
      // 创建下载链接
      const url = window.URL.createObjectURL(new Blob([response.data]))
      const link = document.createElement('a')
      link.href = url
      link.download = `contacts_${new Date().toISOString().split('T')[0]}.xlsx`
      document.body.appendChild(link)
      link.click()
      link.remove()
      
      ElMessage.success('Excel导出成功')
    } catch (error) {
      ElMessage.error('Excel导出失败: ' + error.message)
    }
  }).catch((action) => {
    if (action !== 'cancel') {
      // CSV/JSON导出
      ElMessageBox.confirm('请选择格式', '导出数据', {
        confirmButtonText: 'CSV格式',
        cancelButtonText: 'JSON格式',
        type: 'info'
      }).then(() => {
        exportToCSV(contacts.value)
        ElMessage.success('CSV导出成功')
      }).catch((action2) => {
        if (action2 !== 'cancel') {
          exportToJSON(contacts.value)
          ElMessage.success('JSON导出成功')
        }
      })
    }
  })
}

// 导入功能实现
const handleImport = () => {
  const input = document.createElement('input')
  input.type = 'file'
  input.accept = '.json,.csv,.xlsx,.xls,.txt'
  
  input.onchange = async (e) => {
    const file = e.target.files[0]
    if (!file) return
    
    try {
      const fileName = file.name.toLowerCase()
      
      if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
        // Excel文件处理
        await handleExcelImport(file)
      } else {
        // CSV/JSON文件处理
        const text = await readFileAsText(file)
        let importedContacts = []
        
        if (fileName.endsWith('.json')) {
          importedContacts = parseJSON(text)
        } else {
          importedContacts = parseCSV(text)
        }
        
        await importContacts(importedContacts)
      }
    } catch (error) {
      ElMessage.error(`导入失败:${error.message}`)
    }
  }
  
  input.click()
}

// Excel导入处理
const handleExcelImport = async (file) => {
  loading.value = true
  
  try {
    const formData = new FormData()
    formData.append('file', file)
    
    const response = await axios.post('/api/contacts/import/excel', formData, {
      headers: { 'Content-Type': 'multipart/form-data' },
      baseURL: import.meta.env.VITE_API_BASE_URL
    })
    
    if (response.data.includes('成功导入')) {
      if (response.data.includes('失败')) {
        // 显示详细的导入结果
        ElMessageBox.alert(response.data, '导入结果', {
          type: 'warning',
          showClose: false
        })
      } else {
        ElMessage.success(response.data)
      }
      loadContacts()
    } else {
      ElMessage.warning(response.data)
    }
  } catch (error) {
    ElMessage.error('Excel导入失败: ' + (error.response?.data || error.message))
  } finally {
    loading.value = false
  }
}

4.3 项目运行效果展示

4.3.1 收藏联系人

4.3.2 多种联系方式

img

4.3.3 Excel导入导出

4.3.3.1Excel导入

4.3.3.2Excel导出

img

5. 团队成员分工

团队成员负责模块具体工作内容
贺鸿志后端开发与架构设计1. Spring Boot后端框架搭建
2. RESTful API设计与实现
3. 数据库设计与JPA配置
4.异常处理体系构建
5.单元测试编写
6.云部署配置
吴建远前端开发与UI设计1. Vue 3前端架构设计
2. Element Plus组件集成
3. 响应式界面开发
4.导入导出功能实现
5.数据可视化设计
6.用户体验优化

协作流程:
1.需求分析阶段:共同讨论功能需求和实现方案
2.设计阶段:后端设计API接口,前端设计组件结构
3.开发阶段:并行开发,代码审查
4.测试阶段:交叉测试,确保功能完整性
5.部署阶段:协同配置部署环境

6.评估成员贡献率

团队成员贡献比例评估依据
贺鸿志(832302220)50%1. 完成了核心后端架构和所有API接口
2. 实现了完整的异常处理体系
3. 编写了详细的单元测试
4.配置了多环境部署方案
5. 解决了数据库连接和性能优化问题
吴建远(832302126)50%1. 设计了美观实用的前端界面
2. 实现了复杂的前端交互逻辑
3. 完成了多格式导入导出功能
4.优化了移动端响应式体验
5. 解决了跨域访问和状态管理问题

7.合作困难与解决方案

7.1 成员一:贺鸿志

困难:阿里云RDS连接稳定性问题
在配置Spring Boot连接阿里云RDS MySQL时,频繁出现连接超时和断开问题。特别是在高并发测试时,连接池迅速耗尽,错误日志显示:

com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago.

根本原因分析:
1.网络延迟:ECS与RDS通过内网连接,但存在间歇性延迟
2.连接池配置不当:HikariCP默认配置不适合生产环境
3.RDS白名单限制:安全组未正确配置ECS实例IP
4.MySQL参数限制:wait_timeout设置过短

解决方案:
1.优化连接池配置

# application-prod.yml
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      connection-test-query: SELECT 1
      validation-timeout: 5000

2.增加连接保活机制:

@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties("spring.datasource.hikari")
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.addDataSourceProperty("socketTimeout", "30000");
        dataSource.addDataSourceProperty("connectTimeout", "10000");
        return dataSource;
    }
}

3.配置RDS参数组:

-- 修改RDS参数
SET GLOBAL wait_timeout = 28800;  -- 8小时
SET GLOBAL interactive_timeout = 28800;
SET GLOBAL max_connections = 100;

4.实现连接重试机制:

@Component
public class DatabaseHealthCheck {
    
    @Scheduled(fixedDelay = 300000)  // 每5分钟检查一次
    public void checkConnection() {
        try {
            dataSource.getConnection().close();
        } catch (SQLException e) {
            // 记录告警并尝试重新初始化连接池
            logger.error("数据库连接异常,尝试重连", e);
            reinitializeDataSource();
        }
    }
}

经过优化后,连接稳定性明显提高

7.2 成员二:吴建远

困难:前端与后端跨域通信问题
在阿里云部署环境中,前端(Vue)运行端口与后端(Spring Boot)运行端口不同,浏览器因同源策略阻止跨域请求。特别是在HTTPS环境下,CORS配置更为复杂。

现象:
1.预检请求失败:OPTIONS请求返回403
2.证书问题:前端HTTPS,后端HTTP导致混合内容错误
3.Cookie跨域:Session无法共享
4.WebSocket连接失败:实时功能不可用

解决方案:
采用Nginx反向代理 + 前后端同域 + 详细CORS配置的多层方案:通过 Nginx 作为统一的 HTTPS 入口实现前后端同域访问是解决跨域问题的核心:首先利用 Nginx 配置域名的 HTTPS 证书,将前端静态资源请求和后端 API、WebSocket 请求都收敛到同一个域名和端口下,从根源上消除浏览器同源策略的限制,既解决了前端 HTTPS 与后端 HTTP 并存导致的混合内容错误,也让 Cookie/Session 能够基于同域正常共享。同时,在 Nginx 层面对预检 OPTIONS 请求做缓存处理,减少频繁的预检交互,再配合 Spring Boot 后端的精细化 CORS 配置作为兜底防护,明确允许的请求源、请求方法、请求头和凭证携带规则,解决预检请求 403 的问题;针对 WebSocket 场景,通过 Nginx 开启协议升级支持,配置长连接超时参数,保障实时通信的连接稳定性,最终形成从网络层到应用层的多层跨域解决方案,覆盖协议兼容、凭证共享、实时通信等全场景的跨域问题。

8. 团队成员PSP表

8.1 贺鸿志的PSP表

PSP阶段预估时间(小时)实际时间(小时)备注完成情况
计划11.5细化项目功能需求,评估后端技术难点完成
需求分析47分析项目功能点完成
设计23.5进行数据库设计与调整等完成
编码69.5实现后端接口和前端交互(,处理异常完成
测试22测试基础功能的正常流程和异常场景,进行集成测试等完成
文档24整理后端功能的实现思路,提交至GitHub与CSDN完成
部署12.5使用阿里云服务器进行云部署完成
总计1829功能完善,任务全部完成优秀

8.2 吴建远的PSP表

PSP阶段预估时间(小时)实际时间(小时)备注完成情况
计划11.5细化项目功能需求,评估前端技术难点完成
原型设计35进行UI设计并反复调整完成
编码67实现多种联系方式、导入导出功能等,完善代码完成
测试22测试不同格式Excel文件的导入导出,测试跨设备访问完成
优化23响应式布局优化,配合后端优化功能实现完成
文档24编写部署文档和功能实现说明等完成
部署12.5使用阿里云服务器进行云部署完成
总计1725功能完善,任务全部完成优秀

9. 总结

通过本次极限编程实践,我们团队成功构建了一个功能完整、技术先进的通讯录管理系统。项目采用Vue 3 + Spring Boot全栈架构,实现了联系人收藏、多联系方式管理、Excel导入导出等核心功能,并成功部署至阿里云生产环境。

在开发过程中,我们深刻践行了极限编程方法论:通过结对编程和持续集成确保代码质量,采用测试驱动开发提升系统稳定性。团队成员在跨域通信、大文件处理、移动端适配等技术挑战面前,展现了卓越的问题解决能力和创新思维。

项目不仅满足了所有功能需求,更在代码质量、用户体验和部署运维方面达到了高的标准。我们构建了一套完整的开发运维体系与保障措施。

此次实践让我们将课堂理论知识转化为实际工程能力,为未来的软件开发工作积累了宝贵经验。项目完全达到了课程作业的所有要求,并在技术创新和团队协作方面取得了显著成果。

...全文
257 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复
源码链接: https://pan.quark.cn/s/a4b39357ea24 银行信贷业务作为银行业务的关键构成部分,涵盖了银行向客户提供的各类融资服务,例如贷款、担保以及信用证等。此类业务致力于协助企业与个人解决资金需求问题,进而推动经济活动的开展。银行通过信贷业务获取利息收入,同时需承担相应风险,以保障资金的稳定与流转。 **信贷定义** 信贷意味着银行运用自身资本及信誉为客户提供资金支持,而客户则需以支付利息、费用并偿还本金为前提条件。这种业务模式不仅包含直接贷款,还包括为客户的债务承担提供担保。依照会计准则,信贷业务可分为表内业务与表外业务,前者对银行的资产负债表产生直接影响,后者则不直接关联。 **信贷业务划分标准** 1. **依据会计核算归属**:表内信贷(如贷款、贴现)和表外信贷(如承兑、保证)。 2. **根据期限划分**:短期(不超过1年)、中期(1至5年)与长期(超过5年)。 3. **按照担保方式分类**:信用信贷(无担保)和担保信贷(保证、抵押、质押)。 4. **按照币种区分**:本币信贷与外币信贷。 5. **按照性质和用途区分**:固定资产贷款、流动资金贷款、循环额度贷款、消费贷款等。 6. **按照贷款组织形式区分**:普通贷款、联合贷款和银团贷款。 7. **按照资金来源区分**:信贷资金贷款、委托贷款和境外筹资转贷款。 8. **按照授信对象区分**:公司类信贷与个人类信贷。 **信贷业务产品** 1. **流动资金贷款**:用于企业日常运营周转或临时性资金需求。 2. **固定资产贷款**:用于投资固定资产项目。 3. **房地产开发贷款**:用于土地开发及房屋建设所需资金。 4. **循环额度贷款**:满足企业...

164

社区成员

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

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