软件工程第二次结对作业:校园失物招领系统的设计与实现

软件工程第二次结对作业:校园失物招领系统的设计与实现

项目 内容
这个作业属于哪个课程 202601软件工程
这个作业要求在哪里 2026秋软件工程结对作业(第二次之程序实现)
GitHub 项目地址 102401434-102401436
在线演示地址 校园失物招领系统
项目成员 102401434 包学丰、102401436 冯玄

一、项目背景与目标

校园里的失物招领信息经常分散在不同群聊中,新消息容易覆盖旧消息,同学需要反复翻找才能找到相关内容。物品已经找回或归还后,如果原信息没有及时更新,也容易产生重复询问。

第一次结对作业中,我们完成了需求分析和交互原型。本次作业在已有设计的基础上,实现一个可以实际操作的 Web 应用,围绕以下流程组织功能:

发布信息 → 浏览或搜索 → 查看详情 → 通过联系方式联系发布者 → 发布者更新状态。

本项目使用 HTML、CSS 和原生 JavaScript 实现页面与交互,使用 localStorage 保存本地数据。当前版本面向课程演示,不依赖后台服务器;不同浏览器、不同设备之间的数据不会自动同步。

二、具体分工

成员 主要工作
包学丰 建立项目骨架,实现首页浏览、详情查看及本地存储;整理 README、PSP、测试报告与项目截图;增加交互动画;管理主仓库并合并 PR
冯玄 实现关键词搜索、类型与分类筛选、寻物和招领发布、“我的发布”及状态修改;完善页面样式和测试用例;通过 Fork 和 PR 提交代码

协作时,我们先确定模块职责、数据字段和存储键,再分别实现功能。冯玄通过 Fork 仓库提交修改,我在主仓库检查并合并。对于代码合并后出现的兼容问题,再结合测试结果处理。

三、PSP 表格

以下记录与仓库中的 docs/psp.md 保持一致,时间单位为分钟。

PSP2.1 阶段 具体任务 预估耗时 实际耗时
Planning 制定项目计划和成员分工 20 15
Estimate 估计项目各阶段耗时 15 10
Analysis 分析需求并学习相关技术 45 35
Design Spec 设计页面、数据结构和项目目录 45 35
Design Review 检查页面设计和数据结构 25 15
Coding Standard 制定代码命名和格式规范 20 15
Design 完成功能流程和具体实现设计 70 50
Coding 编写 HTML、CSS 和 JavaScript 代码 360 290
Code Review 检查代码和 Pull Request 50 45
Test 功能测试、单元测试和问题修复 110 80
Test Report 整理测试用例和测试结果 40 35
Size Measurement 统计项目文件和测试数量 15 10
Reporting 完善 README 和作业文档 60 45
Postmortem 总结问题与改进方向 25 20
合计 900 700

预估总耗时为 15 小时,实际总耗时为 11 小时 40 分钟,比预估减少 200 分钟。

第一次作业已经完成需求分析和交互原型,因此本次可以直接依据已有设计实现核心功能。明确模块分工也减少了重复开发。不过,旧数据兼容、存储失败处理、测试路径和文档合并仍产生了额外工作,说明编码之外的检查和整理同样需要预留时间。

四、解题思路与设计实现

4.1 从原型到功能实现

我们把原型中的操作整理为浏览、搜索、详情、发布、发布成功和“我的发布”等视图,并通过 JavaScript 控制视图切换。

实现时优先保证基本流程可用:

  1. 用户可以发布寻物或招领信息。
  2. 首页能够展示已有信息。
  3. 用户可以通过关键词及筛选条件查找物品。
  4. 详情页展示物品特征和联系方式。
  5. 发布者在“我的发布”中结束信息状态。

联系方式用于用户自行通过 QQ、微信或电话等方式联系对方,应用本身不提供即时聊天。

4.2 模块划分

模块 职责
data.js 示例数据、分类和状态等公共常量
storage.js 本地数据读取、保存与旧格式兼容
logic.js 搜索、筛选、校验、信息组装和状态转换
render.js 卡片、详情、列表和错误提示渲染
app.js 页面初始化、导航、事件绑定和操作流程

业务判断集中在 logic.js,尽量通过纯函数实现,使搜索和校验等功能能够脱离真实浏览器进行测试。页面展示与事件处理分别放在其他模块,降低修改样式时影响业务规则的概率。

4.3 数据结构

每条信息主要包含以下字段:

字段 含义
id 信息编号
type 寻物或招领
name 物品名称
category 物品分类
location 丢失或拾取地点
date 丢失或拾取日期
status 当前状态
description 物品特征
contact 联系方式
image 图片 Data URL,可为空
isMine 本地演示中是否属于本人发布
createdAt 信息创建时间

isMine 用于当前浏览器中的“我的发布”展示和状态修改判断。当前没有账户系统,该字段属于本地演示规则,不是服务器验证的身份权限。

4.4 系统流程图

14-system-flowchart

状态更新由发布者从“我的发布”入口操作:

  • 寻物信息:寻找中 → 已找回。
  • 招领信息:待认领 → 已归还。

已结束的信息不能重复修改。保存成功后,首页、搜索结果和“我的发布”同步刷新,使后续浏览者能看到最新状态。

项目中的“已找回”对应题目要求的“已找到”,表达相同的结束状态。

4.5 数据流图

15-data-flow

页面初始化时读取 localStorage,得到信息数组,再交给业务逻辑和渲染模块。

用户发布新信息或修改状态后,程序先保存数据,再刷新展示。保存失败时终止成功流程,避免出现“页面提示成功,但数据实际没有保存”的情况。

五、关键代码及解释

5.1 多条件搜索与筛选

以下代码来自 js/logic.js:

function filterItems(list, filters) {
  const options = filters || {};
  const type = options.type || ALL_TYPE;
  const category = options.category || ALL_CATEGORY;
  const keyword = options.keyword || "";

  const matched = (list || []).filter(function (item) {
    if (!item) {
      return false;
    }

    if (type !== ALL_TYPE && item.type !== type) {
      return false;
    }

    if (category !== ALL_CATEGORY && item.category !== category) {
      return false;
    }

    return matchKeyword(item, keyword);
  });

  return sortItems(matched);
}

关键词、类型和分类采用“同时满足”的关系。例如,搜索“校园卡”并选择“招领”,结果必须同时符合名称或描述匹配和类型匹配。

matchKeyword 对输入进行去除首尾空格和转小写处理,并检查物品名称与描述。空关键词表示不限制关键词;无匹配结果则返回空数组,由页面显示空状态提示。

5.2 本地保存及失败反馈

以下代码来自 js/storage.js:

function saveItems(list) {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
    return true;
  } catch (error) {
    console.warn("物品数据保存失败:", error);
    return false;
  }
}

程序把信息数组序列化为 JSON,保存在 campus-lost-found-items 键中。

保存函数返回布尔值,让调用方能够区分成功和失败。如果浏览器存储空间不足,程序返回 false,而不是继续展示成功提示。

5.3 状态更新失败时恢复原状态

以下代码来自 js/app.js:

function resolveItem(itemId) {
  const result = itemLogic.changeStatus(items, itemId, null);

  if (!result.ok) {
    itemRender.showToast(result.reason);
    return;
  }

  if (!persist()) {
    result.item.status = result.from;
    refreshAll();
    return;
  }

  refreshAll();
  itemRender.showToast(
    "已标记为「" + result.to + "」,首页与搜索结果会同步显示"
  );
}

业务层先判断信息是否存在、是否属于本人发布、是否已经结束,再修改状态。

如果保存失败,就使用 result.from 恢复原状态;只有保存成功,才提示状态更新完成。这样能减少页面状态与本地保存结果不一致的问题。

六、附加特点与成果展示

6.1 分类与组合筛选

除了关键词搜索,用户还可以按物品类型和分类缩小范围。例如,选择“电子产品”和“招领”,可以减少无关结果。

实现上复用前面的 filterItems,避免首页与搜索页分别编写一套筛选规则。

02-search-results

6.2 一键复制联系方式

详情页提供“复制联系方式”按钮,减少用户手动输入号码的操作。

程序优先调用浏览器剪贴板接口,并提供兼容方式。如果复制失败,显示手动记录提示。该功能的实际效果受浏览器权限影响,需要在真实浏览器中检查。

03-item-detail

6.3 图片选择与预览

发布时可以选择物品图片,检查图片类型和 2MB 大小限制,并在提交前预览,帮助用户更清楚地描述物品。

以下代码来自 js/app.js:

function readImageFile(file, done) {
  const reader = new FileReader();

  reader.onload = function () {
    done(typeof reader.result === "string" ? reader.result : "");
  };

  reader.onerror = function () {
    done("");
  };

  reader.readAsDataURL(file);
}

FileReader 把图片读取为 Data URL,后续用于预览并随信息保存。当前图片保存在浏览器本地,不上传至服务器;较大的图片也会占用更多本地存储空间。

04-publish-form

6.4 页面动画与操作反馈

我们为页面切换、卡片出现、按钮点击和成功提示加入轻量动画。例如,页面进入时淡入并轻微上移:

.view:not([hidden]) {
  animation: view-enter 0.22s ease-out;
}

@keyframes view-enter {
  from {
    opacity: 0;
    transform: translateY(8px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

这段代码来自 css/style.css。动画用于帮助用户感知页面变化,同时提供减少动态效果的系统偏好适配。

13-ui-animation

6.5 其他成果展示

系统首页 发布成功
系统首页 发布成功
我的发布 状态修改
我的发布 状态修改

七、目录组织与运行说明

7.1 项目目录

102401434-102401436/
├─ assets/
│  └─ screenshots/       # 功能截图、测试截图、流程图和 GIF
├─ css/
│  └─ style.css          # 布局、样式和动画
├─ js/
│  ├─ data.js            # 示例数据和公共常量
│  ├─ storage.js         # 本地存储
│  ├─ logic.js           # 业务逻辑
│  ├─ render.js          # 页面渲染
│  └─ app.js             # 导航与事件处理
├─ docs/
│  ├─ psp.md             # PSP 预估与实际耗时
│  ├─ psp-复盘.md        # PSP 总结
│  ├─ test-report.md     # 测试报告
│  ├─ 设计核对清单.md
│  └─ 上传时间表.md
├─ tests/
│  ├─ cases.js           # 基础测试用例
│  ├─ index.html         # 浏览器测试入口
│  └─ run-tests.js       # Node.js 基础测试入口
├─ unit-tests/
│  ├─ tests/             # Jest 测试文件
│  ├─ api.js             # 独立模拟提交模块,未接入网页
│  ├─ package.json
│  ├─ package-lock.json
│  └─ README.md
├─ .gitignore
├─ index.html            # 网页入口
└─ README.md             # 项目介绍及使用说明

HTML、CSS、JavaScript 和展示素材分目录保存,测试与文档也分别组织。这样测试人员可以先阅读 README,再定位入口和相关模块。

7.2 本地运行

网页功能不需要构建,也不需要安装 Node.js。请统一使用 Google Chrome 打开。

有 Git 的测试人员可以在 PowerShell 中执行:

git clone https://github.com/tw1l1ghtcc/102401434-102401436.git
Set-Location '.\102401434-102401436'

随后打开 Google Chrome,按 Ctrl+O,选择仓库根目录的 index.html。

没有 Git 时,也可以在 GitHub 点击 Code → Download ZIP,解压后使用相同方式打开。

7.3 基本使用步骤

  1. 首页浏览寻物和招领信息。
  2. 切换类型或选择物品分类。
  3. 进入搜索页,输入关键词并组合筛选条件。
  4. 点击卡片查看物品特征和联系方式。
  5. 通过联系方式自行联系发布者。
  6. 从底部“发布”入口填写寻物或招领信息。
  7. 发布成功后进入“我的发布”。
  8. 找回或归还物品后,标记为“已找回”或“已归还”。
  9. 刷新页面,检查信息和状态是否保留。

数据保存在当前浏览器中。清除浏览器数据会影响已保存的信息,本地文件页面与在线页面的数据也不应视为自动共享。

八、单元测试工具、教程与测试设计

8.1 工具选择和学习路径

本项目使用 Node.js 运行测试,使用 Jest 组织测试用例、断言及模拟函数,使用 jsdom 模拟页面环境。

测试直接读取正式项目源码,不另外复制一份应用代码,避免测试副本通过但正式代码仍有问题。

学习和复现时,可以依次阅读 Jest 入门文档、Jest 断言说明 和 jsdom 项目说明。先理解输入与预期结果,再尝试正常用例,最后补充边界、异常和页面交互用例。

8.2 简易运行教程

首先安装 Node.js,然后在项目根目录运行基础测试:

node .\tests\run-tests.js

运行 Jest 测试:

Set-Location .\unit-tests
npm.cmd install
npm.cmd test -- --runInBand

安装依赖后,后续只需在 unit-tests 目录执行:

npm.cmd test -- --runInBand

Windows PowerShell 中使用 npm.cmd,避免 npm.ps1 被执行策略阻止。--runInBand 用于串行执行测试。

阅读测试代码时,可以把一个用例理解为三个部分:准备输入、调用函数、检查结果。test 定义用例,expect 配合匹配器检查结果;例如 toBe 检查值,toMatchObject 检查对象中的指定字段。Jest 断言说明

8.3 项目测试代码示例

以下片段来自 unit-tests/tests/logic.test.js,依赖该文件中已有的 makeList 测试数据和 L 业务模块:

test("寻物:寻找中 → 已找回", () => {
  const list = makeList();
  const r = L.changeStatus(list, 1, "已找回");

  expect(r).toMatchObject({
    ok: true,
    from: "寻找中",
    to: "已找回"
  });

  expect(list[0].status).toBe("已找回");
  expect(L.isResolved(list[0].status)).toBe(true);
});

这个用例检查 changeStatus 的返回结果,也检查列表中的实际状态,避免函数报告成功却没有更新数据。

以下用例检查非法目标状态:

test("目标状态不合法时拒绝,且不污染数据", () => {
  const list = makeList();

  expect(L.changeStatus(list, 1, "已归还").ok).toBe(false);
  expect(list[0].status).toBe("寻找中");
});

寻物信息不能直接变成招领的“已归还”,拒绝操作后原状态也应保持不变。

8.4 白盒测试与测试数据构造

我们根据业务函数内部的条件判断构造用例,覆盖成功、拒绝和异常分支,同时结合等价类与边界条件设计数据。

场景 测试数据或操作 预期
名称搜索 输入名称中的关键词 返回匹配信息
描述搜索 输入特征描述中的关键词 返回匹配信息
空关键词 输入空字符串 不限制关键词
无匹配结果 输入无关内容 返回空数组
空列表 使用空数组搜索 不报错
组合筛选 同时限定关键词、类型、分类 条件同时生效
必填项缺失 使用空表单 返回字段错误
名称长度 少于 2 字或超过 30 字 拒绝发布
描述长度 少于 5 字或超过 200 字 拒绝发布
日期异常 不存在的日期或未来日期 拒绝发布
图片异常 非图片或超过 2MB 返回校验错误
正常状态修改 本人未结束信息 修改成功
他人信息 isMine 为 false 拒绝修改
重复修改 已找回或已归还的信息 拒绝再次修改
非法目标状态 给寻物设置“已归还” 拒绝且保持原状态
信息不存在 使用不存在的 ID 返回失败,不崩溃
非法存储数据 非法 JSON 或非数组 回退为示例数据
保存失败 模拟存储写入异常 返回失败并提供反馈

面对测试人员可能构造的特殊字符、空白输入、缺失字段和损坏数据,我们优先检查程序是否崩溃、是否误报成功、是否错误修改原数据,而不仅检查正常路线。

8.5 测试结果与评价

本次重新运行的自动化测试结果为:

测试方式 结果
Node.js 基础测试 35 项全部通过,结构契约检查通过
Jest 单元测试 4 个测试文件、67 项全部通过

08-basic-tests

09-jest-tests

67 项 Jest 测试中,58 项针对正式应用,9 项针对尚未接入网页的独立模拟提交模块 api.js。模拟接口测试用于检查请求组装和异常处理,不代表网页已经具备后台服务。

当前用例覆盖主要业务判断、边界输入、部分页面交互和存储异常,满足至少 10 个测试用例的要求。但测试通过不等于没有缺陷,也不等于已经实现全部分支覆盖。

动画、实际剪贴板权限和图片交互仍需要真实浏览器验收。Jest 运行结束后还出现异步操作未及时结束的提示,这是测试环境需要继续清理的问题,与全部用例通过的结果分别记录。

仓库原测试报告记录了 12 项手动验收通过。本次补充确认了 Chrome 可以正常打开本地首页;其余操作的 Chrome 复核应逐项记录,不能仅以自动化测试替代。

九、GitHub 提交记录与结对协作

我们按功能划分提交内容,使首页、详情、本地存储、搜索发布、文档和动画等修改可以追溯。

10-github-commits

主要 PR 记录如下:

PR 内容
#1 首页基础布局和示例信息展示
#2 物品详情查看
#3 浏览器本地数据存储
#4 合并队友的搜索、筛选、发布、状态修改及测试功能
#5 完善目录结构与使用说明
#6 补充测试报告
#7 补充 PSP 实际耗时与总结
#8 增加交互动画
#9 补充截图、测试结果、流程图和 GIF
#10 完善 README 展示与使用说明
#11 修正文档并明确测试范围
#12 统一 PSP 复盘与主表的耗时口径

11-pull-requests

仓库主页集中展示项目介绍、功能效果、目录和运行方法,方便其他同学下载与测试。

12-repository-home

十、遇到的问题、尝试与解决方法

10.1 PowerShell 无法直接运行 npm

问题: 输入 npm 后,PowerShell 因执行策略限制而阻止 npm.ps1。

尝试: 检查报错,确认问题发生在命令入口,而不是项目测试代码。

解决: 使用 npm.cmd install 和 npm.cmd test -- --runInBand,并同步更新文档命令。

收获: 环境问题需要先定位到具体层次,运行说明也应考虑其他同学使用的系统。

10.2 旧版本地数据与新版字段不一致

问题: 旧数据缺少 image、isMine 和 createdAt 等字段,可能影响新版展示和“我的发布”。

尝试: 对比新旧字段,检查数据读取过程。

解决: 在存储模块增加旧格式识别。当前迁移逻辑针对早期仅含示例信息的数据,恢复新版示例数据;损坏数据也提供回退处理。

收获: 调整数据结构时,必须同时考虑已有数据。当前兼容逻辑有适用范围,未来支持真实历史记录时,需要逐条迁移而非直接恢复示例数据。

10.3 保存失败却显示操作成功

问题: 如果存储失败后仍进入发布成功页面或提示状态修改成功,用户会误以为数据已经保存。

尝试: 检查保存函数和成功提示之间的调用顺序,并通过测试模拟写入失败。

解决: 保存函数返回结果。发布失败时移除刚加入的信息;状态更新失败时恢复原状态。

收获: 成功提示应依据实际保存结果,不能仅依据按钮被点击或内存数据被修改。

10.4 测试源码与正式源码不一致

问题: 测试目录曾保留源码副本,修改正式代码后可能未同步副本;测试路径也需要调整。

尝试: 检查测试读取的位置和目录关系。

解决: 让测试直接读取项目根目录的正式源码,并删除文档中“复制源码后测试”的旧说明。

收获: 测试对象必须与交付对象一致,否则通过结果的参考价值会下降。

10.5 文档合并与过程记录不一致

问题: README 合并时需要保留已有成员和原型信息;后期检查还发现图片功能说明、测试范围和 PSP 复盘口径不一致。

尝试: 对照实际代码、测试文件和 PSP 主表逐项核查。

解决: 通过文档 PR 修正说明,区分正式应用测试与 Mock API 测试,并统一 PSP 复盘与主表。

收获: 文档也是交付的一部分。代码变更后,应同步检查 README、测试报告和博客,避免各处描述不同。

十一、队友评价

冯玄负责搜索、筛选、发布和状态修改等功能,使系统从信息展示进一步形成完整的操作流程。他把业务判断拆分为函数,并配合测试用例检查边界情况,这一点值得我学习。

在协作方式上,通过 Fork 和 Pull Request 提交修改,方便我核对变更后合并,也让功能分工有了明确的记录。

需要改进的地方是,功能和数据结构调整时,可以更早同步接口约定及文档说明,减少合并后集中处理兼容问题。测试目录和过程文档也可以在功能提交时同步整理。

对我而言,这次合作提醒我:管理主仓库不只是合并代码,还需要检查不同模块连接后的行为,以及文档是否准确反映最终成果。

十二、项目收获与后续改进

本次作业把第一次作业的原型转化为可运行网页,也让我们认识到,设计一个按钮与实现完整操作之间,还包括输入校验、数据保存、失败反馈和状态更新。

搜索和发布功能能够正常运行,只是其中一部分。空结果、非法日期、重复修改以及保存失败等情况,同样决定了系统是否可靠。

后续可以继续改进以下方面:

  1. 完善 Chrome 下的手动回归记录。
  2. 清理测试中的异步操作,减少退出提示。
  3. 收集同学试用反馈,验证筛选与状态入口是否容易理解。
  4. 优化旧数据迁移策略,保留更完整的历史数据。
  5. 如果扩展为真实服务,再设计后台存储、账户和身份校验。

当前版本已经实现寻物与招领发布、浏览搜索、详情与联系方式展示,以及信息状态维护,并提供可复现的测试命令和使用说明。

posted @ 2026-10-06 15:01  twilduduidis  阅读(10)  评论(0)    收藏  举报