2026秋软件工程第二次结对作业之程序实现——校园失物招领
2026秋软件工程第二次结对作业之程序实现——校园失物招领
结对成员:102402126 黄炜杰、102402127 金若曦
一、作业相关信息
| 项目 | 内容 |
|---|---|
| 结对队友博客链接 | 黄炜杰的博客 |
| 本作业博客链接 | 本作业博客 |
| GitHub 项目地址 | https://github.com/X-runner354/102402126-102402127 |
二、具体分工
| 成员 | 负责内容 |
|---|---|
| 102402126 黄炜杰 | 核心业务逻辑设计(core.js 纯函数)、数据结构与数据模型定义、单元测试设计与编写、README 文档、GitHub 仓库管理 |
| 102402127 金若曦 | 页面结构与样式设计(index.html + style.css)、交互逻辑(app.js 渲染与事件)、数据流图与流程图绘制、附加特点(一键复制、多条件筛选)设计 |
两人通过 Pull Request 协作,代码有进展即签入 GitHub。
三、PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 40 |
| Estimate | 估计这个任务需要多少时间 | 20 | 30 |
| Development | 开发 | ||
| Analysis | 需求分析(包括学习新技术) | 120 | 150 |
| Design Spec | 生成设计文档 | 60 | 70 |
| Design Review | 设计复审 | 30 | 40 |
| Coding Standard | 代码规范(为目前的开发制定合适的规范) | 30 | 25 |
| Design | 具体设计 | 90 | 110 |
| Coding | 具体编码 | 300 | 360 |
| Code Review | 代码复审 | 60 | 50 |
| Test | 测试(自我测试,修改代码,提交修改) | 120 | 180 |
| Reporting | 报告 | ||
| Test Report | 测试报告 | 60 | 70 |
| Size Measurement | 计算工作量 | 20 | 15 |
| Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 40 | 50 |
| 合计 | 980 | 1190 |
差异说明:实际耗时比预估多出约 210 分钟,主要集中在「测试」(+60,编写用例时发现局部变量作用域与筛选组合逻辑有 bug,返工修改)、「具体编码」(+60,页面在部分窗口尺寸下排版错位,反复调整样式)、「需求分析」(+30,围绕"联系之后如何结束信息"补充了状态流转设计)。这说明我们对测试与样式适配的难度有所低估,下次应在预估时预留更多测试与返工余量。
四、解题思路描述与设计实现说明
4.1 代码实现思路
需求理解
本次作业的核心是做一个"校园失物招领"产品,必须跑通一条基本的闭环:
发布信息 → 浏览/搜索 → 查看详情 → 联系发布者 → 更新状态
同时明确不做复杂后台、实名认证、即时聊天、地图定位。结合第一次作业原型,我们把范围收敛到最核心的几个能力上:
- 发布:区分"寻物(lost)"和"招领(found)"两类信息;
- 集中浏览:以卡片列表展示全部信息,徽章区分类型与状态;
- 搜索:按物品名称、地点、描述等关键词检索;
- 查看详情:展示完整字段与发布者联系方式;
- 更新状态:物品找回/归还后,由发布者标记为"已找到"/"已归还"。
技术选型
- 纯前端 SPA:只用一个
index.html,通过 JavaScript 渲染四个视图(首页/发布/我的发布/详情),测试人员用谷歌浏览器直接打开即可运行,零环境依赖。 - 数据存储用 localStorage:无需后端,刷新不丢数据,方便测试与演示。
- 前端分层:把与界面无关的业务逻辑抽成纯函数放入
core.js,界面渲染与事件放入app.js,二者解耦,从而让核心逻辑能被 Node/Jest 直接单元测试。
数据模型
每条信息用统一的对象表示:
{
id, // 唯一标识,generateId() 生成
type, // 'lost'(寻物) | 'found'(招领)
itemName, // 物品名称
category, // 类别:证件/电子产品/文具/衣物/钥匙/生活用品/其他
location, // 地点:教学区/图书馆/食堂/操场/宿舍区/其他
itemDate, // 遗失/拾取日期
description, // 详细描述
contact, // 联系方式
publisher, // 发布者
status, // 'open'(进行中) | 'resolved'(已解决)
createdAt // 创建时间戳,用于排序
}
4.2 关键流程(数据流图)
整体数据流图:

核心业务流程图:

图片托管在 GitHub 仓库
docs/images/,通过 jsDelivr CDN 加速引用,国内访问更快、更稳定。
4.3 重要代码片段及解释
(1)信息校验与创建(core.js)
function validateItem(input) {
const errors = {};
if (!input.type || (input.type !== TYPES.LOST && input.type !== TYPES.FOUND)) {
errors.type = '类型必须是寻物或招领';
}
if (!input.itemName || !String(input.itemName).trim()) {
errors.itemName = '物品名称不能为空';
}
if (!input.category || !String(input.category).trim()) {
errors.category = '请选择物品类别';
}
if (!input.location || !String(input.location).trim()) {
errors.location = '地点不能为空';
}
if (!input.contact || !String(input.contact).trim()) {
errors.contact = '联系方式不能为空';
}
if (!input.publisher || !String(input.publisher).trim()) {
errors.publisher = '发布者不能为空';
}
return { valid: Object.keys(errors).length === 0, errors: errors };
}
解释:validateItem 把"发布信息是否完整"这一规则集中到一个纯函数里,返回是否合法及具体错误字段。前端只负责展示第一条错误提示,测试也能直接调用它覆盖各种边界(空值、非法类型)。这也是上次作业点评里重点提到的"发布信息不完整要有提示"。
(2)搜索与筛选(core.js)
function searchItems(items, keyword) {
const kw = String(keyword || '').trim().toLowerCase();
if (!kw) return items.slice();
return items.filter(function (item) {
return [item.itemName, item.category, item.location, item.description, item.publisher]
.filter(Boolean)
.some(function (field) { return String(field).toLowerCase().indexOf(kw) !== -1; });
});
}
解释:搜索使用 toLowerCase 做大小写不敏感匹配,对物品名、类别、地点、描述、发布者等多字段联合检索;关键词为空时直接返回原列表副本,避免误过滤。
(3)状态更新(core.js)
function updateStatus(items, id, newStatus) {
return items.map(function (item) {
if (item.id === id) {
return Object.assign({}, item, { status: newStatus });
}
return item;
});
}
解释:使用不可变更新,不修改原数组而是返回新数组,便于测试断言"原数据不被污染",也让界面渲染逻辑更可预测。
五、附加特点设计与展示
5.1 设计的创意独到之处及意义
- 一键复制联系方式:失主/拾主常有"看了一遍又得手动背号码"的烦恼,一键复制电话号码直接写入剪贴板,减少手动输入错误,这正是助教提示过的加分点,也贴合"减少无效联系"的产品目标。
- 多条件组合筛选:除了"寻物/招领"标签,还支持按类别、地点、状态三重下拉组合筛选,用户能快速缩小范围,比单一关键词搜索更贴近真实使用。
- 「我的发布」自管理:以发布者姓名为维度集中展示自己的信息,直接提供"标记已解决 / 删除"按钮,把"联系之后如何结束这条信息"这条流程补完整——谁改、从哪改、别人在哪看到结果都清晰可见。
- 状态文案自适应:
statusLabel根据类型自动切换文案——寻物解决显示"已找到"、招领解决显示"已归还",避免"已找到"用在招领物品上的语义混乱。
5.2 实现思路
一键复制实现上考虑了兼容性:优先用 navigator.clipboard.writeText(现代浏览器 API,但 file:// 协议下可能受限),失败时回退到 document.execCommand('copy'),保证在本地直接打开文件也能复制成功。
5.3 代码片段及解释
function copyText(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function () {
showToast('联系方式已复制到剪贴板');
}).catch(function () { legacyCopy(text); });
} else {
legacyCopy(text);
}
}
function legacyCopy(text) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
var ok = false;
try { ok = document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
showToast(ok ? '联系方式已复制到剪贴板' : '复制失败,请手动复制');
}
解释:在无后端、直接 file:// 打开的场景下,新旧的 Clipboard API 行为不一致,这里做了双通道降级,保证任何环境都能成功复制。
5.4 实现成果展示
首页集中展示寻物与招领信息,徽章区分类型与状态,支持关键词搜索与多条件筛选。

输入「校园卡」后仅保留匹配结果,其余信息被过滤。

寻物与招领共用一张发布表单,只差一个类型选项,必填项缺失会提示。

详情页展示完整字段与发布者联系方式,支持一键复制。

发布者在「我的发布」中按姓名筛选自己的信息,并标记「已找到 / 已归还」或删除。

六、目录说明和使用说明
6.1 目录是如何组织的
102402126-102402127/
├── index.html # 入口页面(单页应用,含首页/发布/我的发布/详情四类视图)
├── css/
│ └── style.css # 全局样式:主题变量、卡片、表单、徽章、响应式
├── js/
│ ├── core.js # 核心业务逻辑(纯函数,无 DOM 依赖,便于单元测试)
│ └── app.js # 界面逻辑:渲染、事件、localStorage 读写、视图切换
├── test/
│ └── core.test.js # Jest 单元测试(21 个用例)
├── docs/
│ └── images/ # 数据流图、业务流程图及界面截图(首页/发布/搜索/详情/我的发布)
├── package.json # 项目描述与 Jest 测试脚本
├── CONTRIBUTING.md # 提交规范与协作流程说明
└── README.md # 目录与使用说明
设计原则:界面层与业务层分离。core.js 不依赖浏览器 API,可被 app.js(浏览器全局对象)和 Jest(CommonJS)两种方式加载,同一套业务逻辑既服务页面又服务测试,避免逻辑重复。
6.2 测试人员如何运行网页
- 将整个项目下载到本地;
- 用谷歌浏览器(Chrome)打开
index.html即可,无需联网、无需部署服务器; - 数据保存在浏览器 localStorage 中,刷新不丢失。
操作路径:首页浏览 → 搜索/筛选 → 点击卡片看详情 → 一键复制联系方式 → 点「发布信息」发布一条 → 到「我的发布」输入自己姓名查询 → 标记「已找到/已归还」或删除。
七、单元测试
7.1 测试工具选择与学习过程(附简易教程)
我们选择 Jest:它零配置、语法直观、社区资料多,且完美支持我们"核心逻辑抽成纯函数"的架构——core.js 用 if (typeof module !== 'undefined' && module.exports) 条件导出,浏览器里当全局对象用,Jest 里直接 require。
简易教程(三步跑起来):
- 安装 Node.js;
- 在项目根目录
npm install(会自动安装 package.json 里的 jest); - 写测试文件
test/xxx.test.js,用const LostFound = require('../js/core.js')引入被测代码; - 运行
npm test即可看到全部用例结果。
一个最小例子:
test('add 求和', () => {
expect(1 + 2).toBe(3);
});
断言用 expect(...).toBe(...) / .toEqual(...) / .toThrow(),describe 用来分组。
7.2 单元测试代码展示(说明测试的函数)
针对 core.js 中的 validateItem、createItem、searchItems、filterItems、updateStatus、getItemById、deleteItem、statusLabel、generateId 编写用例,共 21 个,节选如下:
describe('updateStatus / getItemById / deleteItem 状态维护', () => {
const items = [
LostFound.createItem(validInput({ itemName: 'A' })),
LostFound.createItem(validInput({ itemName: 'B', type: 'found' }))
];
test('更新目标条目状态且不修改原数组(不可变)', () => {
const updated = LostFound.updateStatus(items, items[0].id, LostFound.STATUS.RESOLVED);
expect(items[0].status).toBe(LostFound.STATUS.OPEN);
expect(updated[0].status).toBe(LostFound.STATUS.RESOLVED);
expect(updated[1].status).toBe(LostFound.STATUS.OPEN);
});
test('更新不存在的 id 时原样返回', () => {
const updated = LostFound.updateStatus(items, 'not-exist', LostFound.STATUS.RESOLVED);
expect(updated).toEqual(items);
});
test('getItemById 能找到并返回对象,找不到返回 null', () => {
expect(LostFound.getItemById(items, items[1].id).itemName).toBe('B');
expect(LostFound.getItemById(items, 'not-exist')).toBeNull();
});
});
这段测试验证的是"状态维护"三个最关键的纯函数:updateStatus 是否只改目标条目、是否保持不可变;getItemById 的命中与未命中两种情形;deleteItem 是否正确移除。
7.3 构造测试数据的思路
我们按白盒测试为主,覆盖每个函数的分支与边界:
- 正常路径:所有字段齐全、能搜索到、能筛选到、能正确定位;
- 边界输入:空字符串、纯空格、大小写混合、非法 type、不存在的 id、无匹配关键词;
- 异常路径:非法输入
createItem抛出异常、toThrow()断言; - 副作用:
updateStatus验证不可变性,确保原数组不被污染。
为了让测试可读、可维护,用 validInput(overrides) 工厂函数构造"标准合法对象",再按需 overrides 覆盖单个字段,避免每个用例重复拼一大段对象。针对助教可能的"刁难",我们把"空字符串、非法类型、组合筛选无结果、大小写、不存在的 id"这类最容易出 bug 的情况都写成了断言。
八、遇到的代码模块异常或结对困难及解决方法
| 问题描述 | 做过哪些尝试 | 是否解决 | 有何收获 |
|---|---|---|---|
用 file:// 打开时 navigator.clipboard 因非安全上下文失效,无法复制 |
改用 document.execCommand('copy') 降级方案,并做双通道兜底 |
是 | 本地离线场景要考虑浏览器 API 的可用性差异,要做好降级 |
| 搜索实现初期遗漏了"大小写不敏感"导致英文物品名搜不到 | 统一 toLowerCase() 后再 indexOf 匹配 |
是 | 搜索务必显式处理大小写与前后空格 |
| 状态文案"已找到"对招领物品语义不通 | 引入 statusLabel 根据 type 动态返回「已找到/已归还」 |
是 | 文案应与数据状态绑定统一生成,避免手写多处不一致 |
| 结对协作初期没有先约定仓库与目录规范,导致 Push 冲突 | 提前在 README 约定目录结构,统一 feat:/fix: 提交信息,各自按功能切分支 |
是 | Git 协作要"约定先行",功能切分清楚才能少冲突 |
九、评价你的队友
值得学习的地方:队友(黄炜杰)负责核心业务逻辑,把与界面无关的部分抽成纯函数(core.js),设计清晰、可测试性好;单元测试用例想得很全(21 个,覆盖空输入、非法类型、不可变性等边界与异常路径),并主动制定了 Git 提交规范和目录结构,让两人的协作和合并顺畅很多。
需要改进的地方:偏重逻辑实现,对界面视觉细节投入较少,初始配色和排版偏朴素,可以多留意一下 UI 观感;确定技术方案时比较坚持自己的做法,沟通中可以多听听对方对交互体验的想法。
全文完
浙公网安备 33010602011771号