软件工程第二次结队作业
结对编程作业:拾光·校园失物招领 Web 项目
| 项目内容 | |
|---|---|
| 这个作业属于哪个课程 | 202601软件工程 |
| 这个作业要求在哪里 | 2026秋软件工程第二次结对作业之程序实现 |
| 这个作业的目标 | 基于第一次结对作业的原型设计,完成核心功能,使“发布信息—浏览或搜索—查看详情—联系发布者—更新状态”的流程清晰、可用 |
| 学号 | 292400336、182400143 |
| 结对同学博客链接 | 张宇天的博客 |
| GitHub 仓库 | 292400336-182400143 |
一、项目成员与协作过程
| 成员 | 主要工作内容 |
|---|---|
| 吴涵彬(292400336) | 负责项目整体组织、核心功能整合、数据逻辑设计、测试整理以及最终版本维护。 |
| 张宇天(182400143) | 负责页面交互优化、界面细节调整、展示效果完善以及部分文档整理工作。 |
| 双方共同 | 通过 GitHub 协作完成代码交流、功能检查和最终验收。 |
在这次结对开发中,我们采用“共同设计、分工实现、集中完善”的方式推进项目。前期先根据第一次作业中的原型重新梳理需求,明确失物招领系统最重要的不是单纯展示信息,而是让用户能够快速找到目标信息,并完成后续联系和状态更新。
吴涵彬主要负责项目功能实现和整体整合,包括数据结构设计、核心逻辑编写、搜索筛选、状态变化以及测试检查;张宇天主要负责前端页面体验,对布局、交互细节和展示效果进行优化。开发过程中双方会针对功能是否符合使用习惯进行讨论,再决定具体修改方案。
版本管理方面,我们使用 GitHub 保存开发过程。功能完成后及时提交代码,通过提交记录记录阶段成果,并使用 Pull Request 检查合并内容。这样既保留了开发过程,也方便双方发现问题并进行调整。
二、PSP 2.1 时间记录
下表按 docs/PSP.md 明细汇总。Development 和 Reporting 是小计;合计只加 Planning、Estimate 和两个小计,避免重复计数。
| PSP 阶段 | 内容 | 预估(分钟) | 实际(分钟) |
|---|---|---|---|
| Planning | 计划 | 18 | 17 |
| Estimate | 估计任务耗时 | 14 | 11 |
| Development | 开发小计 | 440 | 483 |
| Analysis | 需求分析与技术学习 | 43 | 37 |
| Design Spec | 生成设计文档 | 34 | 30 |
| Design Review | 设计复审 | 19 | 23 |
| Coding Standard | 代码规范 | 14 | 11 |
| Design | 具体设计 | 67 | 78 |
| Coding | 具体编码 | 172 | 187 |
| Code Review | 代码复审 | 29 | 33 |
| Test | 测试和修改 | 62 | 84 |
| Reporting | 报告小计 | 98 | 109 |
| Test Report | 测试报告 | 44 | 51 |
| Size Measurement | 计算工作量 | 15 | 11 |
| Postmortem | 事后总结与改进计划 | 39 | 47 |
| 合计 | 570 | 620 |
实际比预估多 50 分钟,具体设计和测试环节超出较多。设计时补充了状态维护入口,测试时增加了空结果和非法输入等边界情况。下次会在编码前先列出状态变化与异常路径。
三、系统设计思路与功能实现
需求和数据结构
在实际校园环境中,失物信息往往存在发布时间分散、信息不完整、后续状态无法同步等问题。因此本项目围绕“信息发布、快速查找、双方联系、结果反馈”这一完整过程展开设计。
系统整体划分为数据处理层和页面交互层。js/store.js 负责信息保存、校验、搜索、排序以及状态管理;js/app.js 负责页面展示和用户操作响应。每条失物信息包含类型、物品名称、分类、地点、时间、描述、联系方式和当前状态等内容,使一条信息能够完整描述寻找过程。
考虑到课程作业需要轻量运行,本项目采用浏览器本地存储方案。首次进入页面会加载示例数据,用户新增的信息会保存到当前浏览器的 localStorage 中。虽然没有接入服务器和账号系统,但这种实现可以完整展示失物招领业务流程。
核心流程图
图中发布和浏览是两条可独立进入的路径。表单经 validateDraft 检查,createItem 创建对象,saveItems 保存;首页调用 filterItems 产生列表;本机发布者调用 updateStatus 后再次保存。数据只在当前浏览器的 localStorage 中更新,没有服务端或跨设备同步。
关键代码片段
发布前先查缺失字段,再查类型和文本长度。页面与数据层共用这一校验规则。
function validateDraft(draft) {
const required = ['type','title','category','location','date',
'description','contactName','contact'];
const missing = required.filter((key) => !String(draft?.[key] ?? '').trim());
if (missing.length) return { ok:false, message:'请完整填写所有必填项。', missing };
if (!['lost','found'].includes(draft.type))
return { ok:false, message:'信息类型无效。', missing:['type'] };
if (String(draft.title).trim().length < 2)
return { ok:false, message:'物品名称至少填写 2 个字。', missing:['title'] };
if (String(draft.description).trim().length < 5)
return { ok:false, message:'物品描述至少填写 5 个字,便于他人辨认。', missing:['description'] };
return { ok:true, message:'', missing:[] };
}
检索采用多个条件同时满足,关键词在名称、描述、地点和类别中任一命中即可;最终复制数组并按发布时间倒序排列,不改变原始数组。
function filterItems(items, filters = {}) {
const keyword = normalize(filters.keyword);
const type = filters.type || 'all';
const category = filters.category || 'all';
const location = filters.location || 'all';
const status = filters.status || 'all';
return items
.filter((item) => type === 'all' || item.type === type)
.filter((item) => category === 'all' || item.category === category)
.filter((item) => location === 'all' || item.location === location)
.filter((item) => status === 'all' || item.status === status)
.filter((item) => !keyword ||
[item.title,item.description,item.location,item.category]
.some((field) => normalize(field).includes(keyword)))
.slice()
.sort((a,b) => new Date(b.createdAt) - new Date(a.createdAt));
}
更新状态前先检查条目是否存在、演示身份是否匹配。已经解决的信息再次更新时返回副本,以保留原解决时间。
function updateStatus(items, id, actorId = OWNER_ID) {
const index = items.findIndex((item) => item.id === id);
if (index < 0) throw new Error('信息不存在。');
if (items[index].ownerId !== actorId)
throw new Error('只有发布者可以更新这条信息。');
if (items[index].status === 'resolved') return clone(items);
const next = clone(items);
next[index].status = 'resolved';
next[index].resolvedAt = nowIso();
return next;
}
四、项目特色与实际效果
灵活的信息检索。 校园中寻找物品时,用户通常无法记住完整信息,只能提供部分线索。因此项目支持关键词、类别、地点和状态联合查询。用户可以通过多个条件缩小范围,同时系统会给出匹配数量和无结果提示,降低查找成本。
便捷的沟通入口。 失物招领的最终目标是帮助双方建立联系,因此详情页面提供联系方式复制功能。用户查看物品详情后可以直接复制联系方式,避免手动输入产生错误,也让整个流程更加连贯。
async function copyText(text) {
try { await navigator.clipboard.writeText(text); showToast('联系方式已复制'); }
catch (_) {
const input = document.createElement('textarea');
input.value = text;
input.style.position = 'fixed';
input.style.opacity = '0';
document.body.appendChild(input);
input.select();
document.execCommand('copy');
input.remove();
showToast('联系方式已复制');
}
}
下面是实际页面的运行截图:



类型、类别和地点组合筛选后,只剩一条符合条件的招领信息:

发布“银色U盘”后,详情页展示物品信息和联系方式;点击复制按钮后出现成功提示:


同一条信息在“我的发布”中从“进行中”变为“已找到”,回到首页搜索也能看到更新后的状态:



五、目录组织与测试人员使用说明
292400336-182400143/
├─ README.md 项目功能、运行和使用说明
└─ 校园失物招领-结对编程作业/
├─ README.md 网页目录内的同版使用说明
├─ index.html 页面入口和模板
├─ favicon.svg 标签图标
├─ css/styles.css 页面样式与响应式布局
├─ js/store.js 数据校验、筛选、状态与存储
├─ js/app.js 页面渲染、路由和事件
├─ tests/store.test.js 单元测试
├─ docs/PSP.md 时间记录
├─ docs/TEST_REPORT.md 测试说明
├─ docs/BLOG_DRAFT.md 本文草稿
├─ docs/SUBMISSION_CHECKLIST.md 提交检查
├─ screenshots/ 页面截图与流程图
├─ run-tests.bat Windows 测试脚本
└─ 双击打开网页.bat Windows 启动脚本
两份 README.md 现按使用者阅读顺序介绍项目功能、目录、运行、使用和测试方法;成员分工、PSP 时间、测试设计与结对协作过程在本博客和 docs/ 中记录。测试人员下载并完整解压项目后,找到包含 index.html 的网页目录,用 Google Chrome 打开该文件即可。网页无需安装依赖、启动服务器或数据库;Windows 也可以双击 双击打开网页.bat。
建议依次检查:搜索“校园卡”→组合筛选→打开详情→发布一条新信息→进入“我的发布”→标记“已找到”或“已归还”→返回首页确认状态。发布信息保存在当前浏览器。要恢复初始 6 条示例数据,可在 Chrome 开发者工具 Application → Local Storage 删除 campus-lost-found-v1 后刷新。
六、单元测试:工具、简易教程和测试数据
我们使用 Node.js 内置的 node:test 和 node:assert/strict,不需要安装其他测试框架。测试目标是可脱离 DOM 运行的 js/store.js。学习时先看基本断言写法,再对照函数的条件分支设计输入和期望结果,最后运行测试并修正失败用例。
简易教程:安装 Node.js 18 或更新版本,进入项目目录运行 node --test tests\store.test.js。测试文件顶部用 require('node:test')、require('node:assert/strict') 引入工具,再引入被测模块。一个用例按“准备输入 → 调用函数 → 断言输出”编写。若断言失败,终端会给出用例名和错误;修复后重新运行全部测试。
以下是两项真实用例。T08 验证类别与地点同时生效;T12 验证非发布者更新状态被拒绝。
test('T08:按类别和地点组合筛选', () => {
const result = Store.filterItems(Store.seedItems,
{ category:'校园卡/证件', location:'教学楼' });
assert.equal(result.length, 1);
assert.equal(result[0].title, '蓝色校园卡');
});
test('T12:非发布者不能修改他人状态', () => {
const data = [{ ...Store.seedItems[0], id:'other-1',
ownerId:'other', status:'active' }];
assert.throws(() => Store.updateStatus(data, 'other-1', 'me'), /只有发布者/);
});
14 个用例分别检查:完整发布、空联系方式、过短描述、创建后的默认状态、名称搜索、描述与地点搜索、类型筛选、类别加地点筛选、状态筛选、本人更新、招领状态文案、非本人更新、发布时间排序、搜索词空格和英文大小写。测试数据覆盖正常输入、边界输入和异常输入。针对可能的“刁难”,还考虑了不存在的搜索词、多条件无结果和重复更新;其中空结果有页面反馈,重复更新有实现分支,现有自动化用例尚未单独覆盖这两项,应如实区分。
本地执行上述命令得到 14 个通过、0 个失败。另有 Chrome 手工走查记录:发布、详情、复制联系方式、更新状态、返回首页核对。手工走查验证页面操作,单元测试验证数据函数。
下图根据本机实际运行 node --test tests/store.test.js 的标准输出排版生成,保留了 14 项用例及通过统计;它是运行结果图,不是终端窗口的原生截屏。

七、GitHub 代码签入与结对协作
主仓库:endlessmaybe/292400336-182400143;Fork:Ther-ux/292400336-182400143。PR #1:界面与移动端适配 包含 3 次提交,PR #2:目录与使用说明 和 PR #3:README 使用指南 各包含 1 次提交,均已合并。提交信息按改动性质使用 feat:、docs:,例如 feat: refresh campus lost and found interface、docs: add directory and usage guide for testers 和 docs: present project usage without coursework details。
下图截自 GitHub 主仓库的 Commits 页面,可见双方提交、提交信息及三次合并记录;三张 PR 截图显示了已合并状态。




八、开发过程中的问题与收获
问题一:如何体现信息处理完成后的变化。 初始版本主要关注信息展示,但真实使用中找到物品后还需要反馈结果。因此我们增加“我的发布”和状态更新功能,让用户可以把信息从寻找阶段更新到完成阶段。这个过程让我们认识到,一个完整系统不仅需要输入和展示,也需要考虑业务结束后的状态变化。
问题二:搜索功能需要兼顾准确性和易用性。 开发初期发现不同用户输入习惯会影响搜索结果,例如多余空格或大小写差异。因此我们统一处理搜索文本,并增加空结果提示。通过测试不同输入情况,我们进一步认识到异常情况同样属于功能设计的一部分。
协作中的版本衔接:界面改动从 Fork 通过 PR 合入主仓库后,目录说明和 README 也需要跟随最终页面更新。我们用后续两次文档 PR 补齐运行步骤与测试人员使用说明。这个过程提醒我们,功能合并后还要同步检查文档与界面是否一致。
九、对队友张宇天的评价
张宇天在本次结对编程中主要负责页面设计优化和项目展示部分。在合作过程中,他能够从使用者角度观察网页操作是否自然,并针对页面布局、交互细节提出修改意见,使最终作品不仅能够运行,也具有更好的展示效果。
同时,在合作过程中也发现还有提升空间。例如部分页面细节和文档内容如果能够在开发初期同步规划,可以减少后期调整时间。未来进行类似项目时,我们可以提前制定更明确的阶段目标,加强开发过程中的交流,提高整体效率。
十、总结
项目完成了发布、检索、查看联系方式和更新状态的完整本地流程,并以 14 个单元测试和 Chrome 手工走查核对关键路径。如果继续用于真实校园场景,需要服务端存储、账号身份验证和跨设备同步。

浙公网安备 33010602011771号