2026秋软件工程结对作业(第二次)——“校园拾光”程序实现
2026 秋软件工程结对作业(第二次)——“校园拾光”程序实现
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | H202601 软件工程与软件工程实践 |
| 这个作业要求在哪里 | 2026 秋软件工程结对作业(第二次之程序实现) |
| 我的博客/本作业链接 | 章玲102402101 / 本次作业 |
| 结对队友博客/本作业链接 | 吴雅晴102402103 / 本次作业 |
| GitHub 仓库 | 102402101-102402103 |
一、项目简介
“校园拾光”是一个无需后台即可运行的校园失物招领 Web 应用。我们基于第一次结对作业的原型,把“发布信息—浏览或搜索—查看详情—联系发布者—更新状态”实现为可操作的完整流程。用户可以发布寻物或招领信息,通过关键词、类型、类别和状态筛选内容,在详情中查看并复制联系方式;发布者可在“我的发布”中把信息更新为“已找到”或“已归还”。
为保证助教下载后可以直接复现,项目使用原生 HTML、CSS、JavaScript 和浏览器 localStorage,不依赖服务器、数据库或第三方框架,Chrome 直接打开 index.html 即可运行。
二、结对分工
| 成员 | 主要工作 |
|---|---|
| 章玲 | 需求边界、数据模型、store.js 业务逻辑、本地存储、单元测试、测试报告与博客整合 |
| 吴雅晴 | 页面结构、视觉样式、响应式布局、发布/详情/我的发布交互、人工走查与 README 完善 |
| 共同完成 | 功能复审、异常场景补充、Chrome 兼容性测试、Git 提交与最终验收 |
我们的合作方式是“模块主责 + 交叉复审”:完成一个可运行模块后先提交,再由另一位成员按真实用户流程检查,发现问题后通过独立提交修正。
三、PSP
| PSP2.1 | Personal Software Process Stages | 章玲预估/实际(分钟) | 吴雅晴预估/实际(分钟) |
|---|---|---|---|
| Planning | 计划(小计) | 20 / 20 | 20 / 20 |
| Estimate | 估计任务时间 | 20 / 20 | 20 / 20 |
| Development | 开发(小计) | 660 / 820 | 635 / 770 |
| Analysis | 需求分析与技术学习 | 70 / 85 | 65 / 75 |
| Design Spec | 设计文档 | 45 / 55 | 50 / 60 |
| Design Review | 设计复审 | 35 / 45 | 35 / 50 |
| Coding Standard | 制定代码规范 | 25 / 25 | 25 / 25 |
| Design | 具体设计 | 70 / 85 | 90 / 105 |
| Coding | 具体编码 | 230 / 285 | 210 / 255 |
| Code Review | 代码复审 | 55 / 70 | 50 / 65 |
| Test | 自测、修改与提交 | 130 / 170 | 110 / 135 |
| Reporting | 报告(小计) | 130 / 155 | 120 / 145 |
| Test Report | 测试报告 | 60 / 70 | 45 / 55 |
| Size Measurement | 工作量统计 | 20 / 20 | 20 / 20 |
| Postmortem | 总结与改进计划 | 50 / 65 | 55 / 70 |
| 合计 | 仅累加计划、开发、报告三个小计 | 810 / 995 | 775 / 935 |
时间核实说明:上述数值为当前整理稿,需要两位成员根据各自实际记录核实后作为最终 PSP;小计与分项不重复相加。分工和过程描述也应由双方确认。
实际时间高于预估,主要原因是我们低估了表单异常输入、组合筛选和窄屏布局的复测时间。后续会把“异常分支测试”和“多尺寸界面检查”单独列入计划。
四、解题思路与设计实现
4.1 代码实现思路
项目采用简单的三层划分:
store.js是与界面无关的业务层,负责校验、创建、搜索、排序、状态更新、删除与统计。app.js是交互层,负责页面切换、DOM 渲染、表单提交、详情弹窗和localStorage持久化。styles.css是表现层,统一颜色、卡片、表单和响应式断点。
把纯业务函数从 DOM 中分离后,既方便使用 Node.js 自动测试,也降低了修改界面时破坏核心规则的风险。数据模型统一包含 id、type、name、category、location、time、description、publisher、contact、status、owner、createdAt 字段;寻物默认状态为“寻找中”,招领默认状态为“待认领”。
4.2 关键流程图

流程从首页分成浏览/搜索、发布、我的发布三个分支,并把“无搜索结果”“必填项不完整”“提交失败”“状态修改”纳入异常与闭环处理。
4.3 有价值的代码片段
以下搜索函数支持关键词与三种条件组合,并确保结果按发布时间倒序排列:
function searchItems(items, filters = {}) {
const keyword = cleanText(filters.keyword).toLowerCase();
return [...items].filter(item => {
const text = [item.name, item.location, item.description, item.category]
.join(' ').toLowerCase();
return (!keyword || text.includes(keyword))
&& (!filters.type || filters.type === 'all' || item.type === filters.type)
&& (!filters.category || filters.category === 'all' || item.category === filters.category)
&& (!filters.status || filters.status === 'all' || item.status === filters.status);
}).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}
状态更新采用不可变数组,便于测试,也避免原数据被意外修改:
function updateStatus(items, id, status) {
if (!VALID_STATUS.includes(status))
return { ok: false, error: '无效状态', items };
if (!getItem(items, id))
return { ok: false, error: '信息不存在', items };
return {
ok: true,
items: items.map(item => item.id === id ? { ...item, status } : item)
};
}
五、附加特点设计与展示
5.1 多条件组合筛选
除了题目要求的关键词搜索,我们增加了信息类型、物品类别和处理状态筛选。它能让寻找校园卡的用户排除耳机、书本等无关信息,也能隐藏已经解决的记录,减少查找成本。实现上由统一的 searchItems 函数逐项判断,各条件既可单独使用,也可任意组合。
5.2 一键复制联系方式
详情页不会引入超出作业范围的即时聊天,而是使用浏览器 Clipboard API 一键复制发布者提供的联系方式;若浏览器禁止剪贴板权限,会直接显示可手动复制的内容。这既保留了最核心的联系能力,也避免增加后台服务。
实现时先调用 Clipboard API,失败时通过提示框给出可手动复制的原文,不会因为浏览器权限不同而中断流程:
document.querySelector('#copy-contact').addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(item.contact);
toast('联系方式已复制');
} catch {
toast(`请手动复制:${item.contact}`);
}
});
5.3 本地持久化与隐私提示
发布和状态修改都会写入 localStorage,刷新页面后数据仍然存在;发布页明确提醒用户不要填写证件号、支付密码等敏感信息。该设计不需要服务器,便于评分者复现,也符合本次“不做复杂后台”的范围。
5.4 实现成果
首页与信息浏览

搜索与组合筛选

发布信息

物品详情与联系方式

我的发布与状态闭环

六、目录与使用说明
102402101-102402103/
├─ index.html # 应用入口
├─ css/styles.css # 样式与响应式布局
├─ js/store.js # 业务逻辑
├─ js/data.js # 演示数据
├─ js/app.js # 页面交互与本地存储
├─ tests/store.test.js # 16 个自动化测试
└─ docs/ # 博客、测试报告、流程图和截图
测试人员下载全部文件后,用 Google Chrome 打开 index.html 即可。若需要通过本地服务器运行,可在项目目录执行 python -m http.server 8080,然后访问 http://localhost:8080。单元测试使用 Node.js 18+:
具体操作:打开顶部 GitHub 仓库,点击 Code → Download ZIP,解压完整文件夹,在文件夹中找到 index.html,右键选择“打开方式 → Google Chrome”。不要只下载 HTML 文件,CSS 与 JavaScript 必须保留原有相对路径。浏览网页不需要安装 Node.js;只有运行下面的自动化测试时才需要 Node.js,并在包含 index.html 的文件夹打开终端执行命令。
人工验收顺序:进入信息大厅浏览卡片;输入关键词并组合筛选;打开详情核对描述并复制联系方式;发布一条填写完整的寻物或招领信息;进入“我的发布”修改状态;回到信息大厅确认状态同步,刷新后再次确认记录保留。补充检查空名称、短描述、搜索无结果和剪贴板不可用等分支。
数据保存在当前浏览器的 localStorage 中,不同电脑不会自动共享发布内容;本次交付用于本地演示和测试。示例中的学号式联系方式为演示数据,发布页与详情页使用同一条“白色保温杯”记录,便于核对字段传递。
node --test tests/store.test.js
七、单元测试
7.1 工具与简易教程
我们选用 Node.js 内置的 node:test 与 node:assert/strict。学习步骤如下:
- 在业务文件末尾通过 CommonJS 导出纯函数。
- 在测试文件中
require('../js/store.js')。 - 使用
test('说明', () => { ... })定义用例。 - 使用
assert.equal、assert.deepEqual或assert.ok判断实际结果。 - 执行
node --test tests/store.test.js;退出码为 0 且全部显示通过即成功。
7.2 测试代码示例
test('合法状态更新成功且不改变原数组', () => {
const result = core.updateStatus(items, '1', '已找到');
assert.equal(result.ok, true);
assert.equal(result.items[0].status, '已找到');
assert.equal(items[0].status, '寻找中');
});
test('非法状态不会写入', () => {
assert.equal(core.updateStatus(items, '1', '已删除').ok, false);
});
7.3 测试数据设计与“刁难”场景
我们采用白盒分支覆盖,同时结合等价类与边界值:完整表单/空名称、描述恰好不足 5 字、有效/无效时间、存在/不存在 ID、合法/非法状态、名称/地点关键词、有/无结果、单条件/组合条件。测试人员可能输入空格、搜索不存在的物品、反复更新状态或传入非法状态,因此核心函数会先清理文本并校验枚举值。当前共有 16 个测试,全部通过。
示例中的 updateStatus 测试检查合法状态更新、原数组不被修改,以及非法状态被拒绝。16 个用例还覆盖 validateItem、创建信息、searchItems、删除和统计等纯业务函数。
7.4 覆盖范围与评价
正文列出以下 5 个代表性用例,完整 16 个用例见文末附录,与仓库 tests/store.test.js 一一对应:
| 用例 | 输入/操作 | 预期结果 |
|---|---|---|
| T01 正常校验 | 完整合法表单 | valid === true |
| T02 空白名称 | 名称为两个空格 | valid === false |
| T03 描述边界 | 描述为“太短”(不足 5 字) | 返回描述错误 |
| T10 组合筛选 | 数码产品且已归还 | 仅返回 ID 3 |
| T12 状态更新 | ID 1 改为已找到 | 更新成功,原数组仍为寻找中 |
白盒测试根据业务函数的判断分支设计:校验成功/失败、ID 存在/不存在、状态合法/非法,以及筛选条件匹配/不匹配。16 个自动化用例能验证这些核心业务规则,但没有覆盖全部分支,也没有覆盖 DOM 点击、图片选择、浏览器存储配额或剪贴板权限,不能仅凭单元测试通过认定界面全部正常。上述浏览器行为应结合人工验收步骤检查,后续可增加自动化浏览器测试。
八、GitHub 签入记录
项目仓库:Luminance-l/102402101-102402103,完整提交记录可在 Commits 页面 查看。我们按功能拆分提交,保证每次提交只对应一类可验证的增量:
| 提交 | 主要内容 |
|---|---|
f569c4b |
建立数据模型与核心业务规则 |
402d84f |
完成响应式页面和主要用户流程 |
f57895e |
增加 16 个自动化测试与使用说明 |
465d475 |
补充博客、测试说明和程序流程图 |
3c068ac |
整理运行截图与验收清单 |
a1acd10 |
优化博客证据、图片尺寸和真实案例展示 |
队友需要使用自己的 GitHub 账号 fork 本仓库,在独立分支完成一次真实复审修改并向主仓库提交 Pull Request;合并后,本节补充 commits 列表与 Pull Request 截图。这样 GitHub 记录能够真实体现两人的独立操作和代码交互。
待补证据:GitHub commits 页面截图、吴雅晴提交的 Pull Request 截图及链接。
九、异常、结对困难及解决方法
9.1 界面与业务逻辑耦合
- 问题:一开始把输入检查和数据修改都放在按钮的点击事件里,想单独测试某条规则时不方便。
- 尝试:先通过页面操作逐项检查,但每改一次都要重新点击验证,不便于重复测试。
- 解决:把校验、搜索和状态更新抽成
store.js纯函数,界面只调用结果。 - 收获:可测试性应在设计阶段考虑,而不是编码结束后补测试。
9.2 筛选条件相互覆盖
- 问题:只输入关键词时结果正常,同时选择类别和状态后,结果有时与预期不一致。
- 尝试:分别检查搜索框和下拉框的处理逻辑,发现它们各自过滤数据,后一次可能只在上一次的结果中继续查找。
- 解决:每次都从完整数据出发,由一个函数同时应用全部条件,并增加组合筛选测试。
- 收获:多个 UI 条件应对应一个确定的数据入口,避免隐藏状态。
9.3 状态闭环容易遗漏
- 问题:原型重点放在发布和详情,“联系之后如何结束信息”不够明确。
- 尝试:对照完整用户流程,从“联系完成”反向检查首页、详情和“我的发布”中是否能辨认信息的处理状态。
- 解决:在“我的发布”中按寻物/招领分别提供“已找到/已归还”,更新后首页状态同步改变。
- 收获:完成页面不等于完成业务,必须从用户任务的终点反查数据状态。
十、队友评价
吴雅晴在界面层级、卡片布局和交互一致性方面很细致,能够从普通用户视角发现信息密度和提示语问题,也会主动复查不同宽度下的显示效果。值得我学习的是她愿意反复走查页面而不是只看代码。可以改进的是编码任务开始前应更早确定组件命名和样式变量,减少后期统一样式的返工。
十一、总结与改进
本次结对让我们从“能画出原型”走到“能运行、能测试、能复现”。后续若继续开发,会增加图片压缩、编辑已发布内容和更完善的数据导入导出,但仍会优先维护当前核心流程的稳定性,不盲目加入地图、聊天或实名认证等超出范围的模块。
AI 使用说明:本项目在需求整理、代码检查和博客 Markdown 排版阶段使用了 AI 辅助;功能取舍、运行验证、测试数据与结对过程均由成员核对,最终代码需由两位成员共同理解并维护。
十二、提交前待核实事项
- 过程描述已根据本次协作情况整理;两位成员仍需核实 PSP 数字是否符合实际投入。
- 补充 GitHub 签入记录截图以及真实 fork/PR 证据。
- 在班级群结对统计表填写两人信息和仓库地址,并由两人分别在作业页面提交各自博客链接。正常截止时间为 2026 年 10 月 10 日 23:59:59。
附录:完整自动化测试用例
测试夹具包含校园卡(寻物、寻找中)、钥匙(招领、待认领)、耳机(招领、已归还)三条记录。测试以业务函数内部判断为依据,兼顾正常、异常与边界输入。
| 编号 | 测试内容/输入 | 预期结果 |
|---|---|---|
| T01 | 合法完整表单 | 校验通过 |
| T02 | 名称为两个空格 | 校验失败 |
| T03 | 描述“太短” | 描述错误 |
| T04 | 时间 not-a-date |
时间错误 |
| T05 | 创建寻物信息 | 初始状态寻找中 |
| T06 | 创建招领信息 | 初始状态待认领 |
| T07 | 关键词“校园” | 返回 ID 1 |
| T08 | 关键词“ 图书馆 ” | 去除首尾空白,返回 ID 2 |
| T09 | 类型 found |
返回两条招领记录 |
| T10 | 类别数码产品、状态已归还 | 返回 ID 3 |
| T11 | 无筛选条件 | 按时间倒序返回 2、1、3 |
| T12 | 更新 ID 1 为已找到 | 成功且不改变原数组 |
| T13 | 更新不存在的 ID 404 | 返回失败 |
| T14 | 更新为非法状态已删除 | 返回失败 |
| T15 | 删除已有 ID 2 | 成功,剩余两条 |
| T16 | 统计三条夹具记录 | 总数 3、未解决 2、已解决 1 |
完整可执行测试代码:tests/store.test.js。使用第七节命令一次运行全部 16 个用例,断言失败会显示对应名称并返回非零退出码。
浙公网安备 33010602011771号