2026秋软件工程第二次结对作业之程序实现
2026秋软件工程结对作业:校园寻物系统的程序实现
一、项目与结对信息
| 项目 | 内容 |
|---|---|
| 课程 | H202601软件工程与软件工程实践 |
| 作业要求 | 2026秋软件工程结对作业(第二次之程序实现) |
| 结对成员 | 102401115林鑫 102401118卢志勇 |
| GitHub 仓库 | https://github.com/209691048/102401118-102401115 |
| 在线预览 | https://209691048.github.io/102401118-102401115/ |
本项目基于第一次结对作业的原型,完成“校园失物招领”Web 系统的核心流程:发布寻物或招领信息、浏览和搜索、查看详情并联系发布者,以及由发布者更新和维护信息状态。详情页支持一键复制联系方式;寻物标记为“已找到”、招领标记为“已归还”后,卡片会淡化显示,发布者还可在 3 分钟内撤销失误操作。
二、具体分工
| 成员 | 主要工作 |
|---|---|
| 林鑫 | 实现独立编辑页、站内弹窗、成功提示自动消失、完成状态卡片淡化、3 分钟内撤销“已找到/已归还”、联系方式补齐与一键复制、搜索提示等改进;提供截图和复现步骤,回归浏览器测试,在个人分支提交功能及测试改进,跟进 GitHub Actions/PR,。 |
| 卢志勇 | 围绕试用过程梳理并验收需求;创建并维护主仓库,搭建 Web 项目初始结构;负责基础页面、数据及发布、浏览、搜索、详情等核心流程的建设,完善初始测试,参与功能集成和主分支维护,并整理项目说明与博客 |
我们通过 GitHub 分支和 Pull Request 协作。功能完成后先提交到各自分支,再由队友互相检查后合并到主仓库。
三、PSP 时间记录
| PSP 阶段 | 工作内容 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning / Estimate | 明确任务范围、拆分功能并估时 | 45 | 50 |
| Analysis | 阅读题目、梳理用户流程与异常场景 | 100 | 120 |
| Design Spec / Review | 设计页面结构、数据字段和状态流转 | 120 | 150 |
| Coding Standard | 约定目录、命名和提交信息规范 | 30 | 25 |
| Coding | 页面、数据读写与交互功能实现 | 480 | 620 |
| Code Review | 检查改动、处理 PR 反馈和合并问题 | 110 | 180 |
| Test | 手动走查、Node 测试、浏览器测试与 CI 排错 | 240 | 360 |
| Test Report | 整理测试用例、结果与截图 | 90 | 120 |
| Reporting | 完善 README、流程图和博客 | 180 | 210 |
| Postmortem | 复盘问题并制定后续改进计划 | 45 | 55 |
| 合计 | 1440 | 1890 |
实际耗时比预估多 450 分钟,主要集中在编码、代码检查和测试。独立编辑页和站内弹窗需要同时调整页面切换、字段回填和取消逻辑;“已找到 / 已归还”撤销、卡片淡化与复制联系方式是在试用后补充的功能;CI 又先后出现测试文件未提交、VM 环境缺少定时器等问题,需要结合 Actions 日志反复定位。之后会在编码前先列出异常流程和测试场景,并为每项功能的联调、回归测试单独预留时间,减少后期集中返工。
四、需求分析与设计
4.1 解决的问题
校园失物信息常散落在聊天群中,消息容易被覆盖,后续查找和确认状态不方便。系统把寻物和招领信息集中展示,让用户可以搜索物品、按信息类型筛选、查看详情和联系方式。物品找回或归还后,发布者可以更新状态,减少重复询问。
4.2 功能范围
本次完成的核心功能:
- 发布寻物或招领信息,并填写物品名称、地点、日期、描述和联系方式。
- 浏览信息、按关键词搜索,并按“寻物 / 招领”筛选。
- 查看物品详情和发布者联系方式。
- 在“我的发布”中查看本人发布的信息。
- 编辑、删除本人信息;寻物可以标记为“已找到”,招领可以标记为“已归还”。
- 两种完成状态的卡片都会淡化显示,发布者可在标记后的 3 分钟内撤销,恢复为“待处理”;超过时限后撤销入口隐藏。
- 详情页提供“联系发布者”和“复制联系方式”操作;复制失败时会显示可手动复制的联系方式。
4.3 主要数据结构
一条信息使用 JavaScript 对象保存,包含物品内容、发布者标识和当前状态。数据由浏览器 localStorage 保存。statusUpdatedAt 记录完成状态的更新时间,用于判断 3 分钟撤销期限。
{
id, publisherId, title, type, location, date,
description, contactName, contact, icon, status, statusUpdatedAt
}
4.4 核心功能流程图
下图展示了从首页进入浏览、发布和个人信息维护的主要路径,以及表单校验、本人归属检查、取消操作和状态更新等分支。

- 联系流程:详情页 → 查看联系方式 → 联系发布者,或复制联系方式后粘贴使用。
- 状态流程:我的发布 → 标记已找到 / 已归还 → 对应卡片淡化 → 3 分钟内可撤销并恢复待处理;超过 3 分钟后撤销入口消失。
五、代码实现思路
5.1 页面与数据
项目使用原生 HTML、CSS 和 JavaScript。index.html 包含首页、详情、发布、我的发布和编辑页面;js/app.js 负责页面交互和业务逻辑;js/data.js 负责数据读写。发布记录保存在当前浏览器的 localStorage 中。
5.2 发布者归属校验
每条新发布的信息会保存一个 publisherId。个人发布列表只筛选当前浏览器身份对应的记录;编辑、删除和状态更新前都会检查归属。这里的身份标识用于本地原型演示,不等于服务器端账号认证。
function isOwnItem(item) {
const publisherId = getCurrentPublisherId();
return Boolean(publisherId && item && item.publisherId === publisherId);
}
function requireOwnership(item) {
if (!item) {
alert("未找到这条信息。");
return false;
}
if (!isOwnItem(item)) {
alert("只能修改自己发布的信息;旧版无归属信息仅供浏览。");
return false;
}
return true;
}
这段逻辑把“记录是否存在”和“记录是否属于当前用户”作为维护操作的前置条件,避免仅凭页面按钮控制权限。
5.3 编辑内容校验与保存
编辑页面保存时会重新读取数据并检查归属。必填字段会去除首尾空格后校验;保存时只更新允许修改的字段,保留记录 ID、发布者、类型和完成状态。
const values = {};
Object.entries(fields).forEach(function ([suffix, key]) {
values[key] = document.getElementById("edit" + suffix).value.trim();
});
if (!values.title || !values.location || !values.date ||
!values.description || !values.contact) {
showEditError("物品名称、地点、日期、描述和联系方式不能为空。");
return;
}
// 只更新可编辑字段,保留 ID、发布者、类型和状态。
Object.assign(item, values);
saveItems(items);
5.4 状态维护与限时撤销
寻物完成后显示“已找到”,招领完成后显示“已归还”。两种状态都会写入 statusUpdatedAt,更新后重新渲染首页和“我的发布”,并给出站内弹窗提示。卡片共用 .item-completed 样式,降低完成信息的视觉高亮。重复标记时会提示用户,不重复写入相同状态。
发布者在完成状态写入后的 3 分钟内可以撤销;撤销后状态恢复为“待处理”,时间戳清除,卡片也恢复正常显示。超过 3 分钟后,页面隐藏撤销入口,执行撤销时还会再次检查时间和记录归属。
const COMPLETION_UNDO_WINDOW_MS = 3 * 60 * 1000;
function canUndoCompletedStatus(item, now) {
if (!item || (item.type !== "寻物" && item.type !== "招领") ||
getItemStatus(item) !== getCompletedStatus(item)) return false;
const markedAt = Number(item.statusUpdatedAt);
const currentTime = typeof now === "number" ? now : Date.now();
return Number.isFinite(markedAt) && markedAt <= currentTime &&
currentTime - markedAt <= COMPLETION_UNDO_WINDOW_MS;
}
5.5 一键复制联系方式
详情页的复制按钮读取已去除首尾空格的联系方式。浏览器支持剪贴板 API 时优先使用该 API;本地直接打开 HTML 或权限受限时,尝试创建临时文本框并执行复制。成功和失败都会通过站内弹窗反馈,失败时同时展示联系方式供手动复制。
async function copyTextToClipboard(value) {
const text = String(value || "").trim();
if (!text) throw new Error("没有可复制的联系方式。");
if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
try {
await navigator.clipboard.writeText(text);
return;
} catch (error) {
// 本地打开 HTML 或剪贴板权限受限时,尝试兼容方式。
}
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.setAttribute("aria-hidden", "true");
textarea.style.position = "fixed";
textarea.style.opacity = "0";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
let copied = false;
try {
copied = typeof document.execCommand === "function" && document.execCommand("copy");
} finally {
textarea.remove();
}
if (!copied) throw new Error("浏览器未能访问剪贴板。");
}
六、附加特点
除了作业要求的核心流程,我们还完成了以下改进:
- 按信息类型筛选: 可以只看寻物或招领记录,减少无关信息。
- 独立编辑页面: 编辑操作在站内页面完成,不使用浏览器原生输入框;取消或返回不会保存修改。
- 操作弹窗与反馈: 状态更新、删除确认等操作通过页面内弹窗提示;编辑成功提示会在短暂显示后消失。
- 输入校验: 联系方式必填,拒绝纯空格,并限制字段长度。
- 限时撤销: 已找到和已归还状态都允许发布者在 3 分钟内撤销误操作。
- 完成卡片淡化: 两种完成状态在首页和“我的发布”中都会降低饱和度和透明度,方便区分待处理信息。
- 复制联系方式: 提供一键复制、剪贴板兼容方案和失败后的手动复制提示。
这些改进主要是为了让发布者更容易维护信息,也让用户更清楚操作是否成功。
七、目录说明与运行方法
7.1 目录结构
项目根目录/
├── index.html
├── README.md
├── css/
│ └── style.css
├── js/
│ ├── app.js
│ └── data.js
├── tests/
│ ├── test.html
│ ├── browser-test.js
│ ├── run-browser-test.cjs
│ └── ownership-test.cjs
└── .github/
└── workflows/
└── test.yml
7.2 运行主程序
- 下载或克隆主仓库。
- 保持目录结构不变。
- 使用 Google Chrome 打开项目根目录下的
index.html。
项目不需要安装前端框架。数据保存在当前浏览器中,不会自动同步到其他浏览器或设备。
7.3 运行浏览器测试
在项目根目录打开终端,执行:
python -m http.server 8000
然后在 Chrome 打开:
http://localhost:8000/tests/test.html
点击“开始测试”运行浏览器测试。测试页通过 iframe 加载主页面,因此建议通过本地 HTTP 服务运行测试页。
Node 逻辑测试可在项目根目录执行:
node tests/ownership-test.cjs
GitHub Actions 还会运行语法检查、Node 测试和 Chromium 浏览器测试。
八、单元测试与测试用例
8.1 测试工具与学习过程
我们使用 Node.js 的断言和 VM 模拟环境检查业务逻辑,并使用 Playwright 启动 Chromium 测试实际页面交互。测试过程中先阅读工具文档和已有示例,再为项目中的发布、归属校验、编辑、状态更新等功能编写用例。
本地运行命令:
node tests/ownership-test.cjs
node tests/run-browser-test.cjs
浏览器界面也可以通过前述 python -m http.server 8000 方式手动测试。
8.2 测试用例
当前浏览器自动化测试共 28 项,由 Playwright 启动 Chromium 后逐项操作实际页面。
| 编号 | 测试内容 | 预期结果 |
|---|---|---|
| 1 | 首页加载 | 显示 4 条预置记录 |
| 2 | 关键词搜索 | 搜索后仅显示匹配内容 |
| 3 | 招领筛选 | 只显示招领信息 |
| 4 | 搜索无结果 | 显示无匹配提示 |
| 5 | 有效发布 | 保存记录并显示成功结果 |
| 6 | 联系方式只输入空格 | 阻止发布 |
| 7 | 编辑并保存 | 打开独立编辑页并更新记录 |
| 8 | 取消编辑 | 不保存本次修改 |
| 9 | 标记完成状态 | 更新状态并显示站内弹窗 |
| 10 | 重复标记完成状态 | 提示已完成,不重复写入 |
| 11 | 删除确认 | 弹窗取消时保留,确认后删除 |
| 12 | 已归还卡片与撤销 | 卡片淡化,3 分钟内可撤销 |
| 13 | 已归还状态超过 3 分钟 | 隐藏撤销入口 |
| 14 | 大小写及多字段搜索 | 名称、描述、地点搜索及类型组合筛选正确 |
| 15 | 空列表和无匹配 | 分别显示对应提示 |
| 16 | 必填字段为空白 | 阻止提交 |
| 17 | 点击已经过期的撤销入口 | 再次校验时间,不修改状态 |
| 18 | 他人、无归属和过期记录 | 不提供有效撤销操作 |
| 19 | 标题包含 HTML 字符 | 按文本显示,不执行注入内容 |
| 20 | 联系发布者 | 显示联系方式站内弹窗,不调用浏览器原生弹窗 |
| 21 | 联系方式缺失 | 显示缺少联系方式的站内提示 |
| 22 | 已找到状态 3 分钟内撤销 | 状态恢复待处理,卡片恢复正常显示 |
| 23 | 已找到状态超过 3 分钟 | 隐藏撤销入口 |
| 24 | 旧版已找到记录 | 首次打开时补记时间并提供一次撤销机会 |
| 25 | 旧版示例数据缺少联系方式 | 自动补齐演示联系人和邮箱 |
| 26 | 一键复制成功 | 复制正确内容并显示成功提示 |
| 27 | 剪贴板权限受限 | 使用兼容复制方式完成操作 |
| 28 | 剪贴板和兼容方式都失败 | 显示手动复制内容 |
8.3 测试数据与测试设计
测试数据包含本人记录、其他用户记录和没有发布者标识的旧版记录。这样可以检查正常路径,也能检查边界与异常路径。对于输入校验,测试空字符串、纯空格和超长联系方式;对于维护操作,测试合法 ID、他人 ID、旧数据 ID 和不存在的 ID。
目前测试覆盖了主要功能路径,但还可以增加不同浏览器存储限制、键盘操作、窄屏布局和跨浏览器数据差异等测试。由于数据保存在本地浏览器,本版本无法测试多设备同步。
九、代码提交与协作过程
我们使用 Fork、分支和 Pull Request 进行协作。每次完成一组功能后提交代码,通过 PR 让队友检查,再合并到主仓库。
PQ#4 PQ#5 因为发生了冲突导致了关闭与撤销 PQ#1是进行PQ的初次尝试
- Commit 记录截图:
![1f5507273065fec61b11dd5b2332acf2]()
PQ#2的commit没有合并,导致会有点混乱
- 自动化测试记录:
![image]()
十、遇到的问题与解决方法
10.1 编辑按钮未打开编辑页
- 问题描述: 点击编辑后没有进入独立编辑界面。
- 做过的尝试: 检查编辑按钮事件、页面元素 ID 和测试页加载情况;通过浏览器测试复现。
- 解决情况: 补全页面切换和字段回填逻辑,并增加“编辑页是否打开、保存是否生效、取消是否不保存”的浏览器测试。
- 收获: 不能只确认按钮存在,还要验证点击后的完整用户流程。
10.2 GitHub Actions 找不到测试文件
- 问题描述: 工作流提示
MODULE_NOT_FOUND,无法找到tests/ownership-test.cjs。 - 做过的尝试: 对照仓库中的目录结构、工作流命令和文件名,确认实际文件是否已提交到触发工作流的分支。
- 解决情况: 把测试文件提交到分支。
- 收获: 本地存在文件不代表 GitHub 运行的提交也包含该文件,应检查 Actions 对应的 commit 和分支。
10.3 Node 测试模拟环境缺少定时器
- 问题描述: 编辑保存后的成功提示调用
setTimeout,而 VM 测试环境没有提供该函数。 - 做过的尝试: 查看 Actions 错误堆栈,确认错误发生在测试模拟环境。
- 解决情况: 在 VM 上下文中补充定时器函数,使测试环境具备页面逻辑所需的运行能力。
- 收获: 测试环境需要模拟被测代码真实依赖的浏览器 API。
十一、队友评价
对卢志勇的评价
值得学习的地方: 卢志勇先搭建了仓库、页面和数据的基础结构,把发布、浏览、搜索和详情等核心路径做成了可运行的版本,后续功能可以在此基础上逐步迭代。遇到主分支集成和测试问题时,也能够配合检查并维护主仓库。
需要改进的地方: 后续可以更早同步数据字段、页面入口和每次提交覆盖的功能,尤其是在新增状态字段或调整页面结构前先说明影响范围。这样双方编写测试和处理 PR 冲突时会更顺畅。
十二、总结与后续计划
这次结对作业让我经历了需求拆分、页面实现、测试设计、PR 协作和问题排查。实现过程中,我们补充了完成状态的 3 分钟撤销、已找到与已归还卡片淡化、联系方式复制和旧示例联系方式迁移,并为这些交互增加了浏览器测试。页面看起来完成并不代表交互已经可靠,仍要通过手动走查和自动化用例验证。
当前项目使用浏览器 localStorage 保存数据,适合本地演示。后续若要让全校用户共同使用,需要增加后端、数据库和真正的用户认证,并在服务器端校验数据归属。



浙公网安备 33010602011771号