软件工程第二次结对作业
软件工程第二次结对作业之程序实现
| 项目 | 内容 |
|---|---|
| 课程 | H202601软件工程与软件工程实践 |
| 作业要求 | 2026秋软件工程结对作业(第二次之程序实现) |
| 作业目标 | 将校园失物招领原型实现为可运行的 Web 应用,完成核心业务流程,并通过 GitHub 协作、PSP 和单元测试实践结对开发、版本管理与软件测试 |
| 学号姓名 | 102401614 林志涛、 102401625 朱铮睿、 |
| GitHub 仓库 | https://github.com/Ruiii1124/102401625-102401614 |
| 队友博客 | https://www.cnblogs.com/Ruiiiiiiiiiii/p/23190772 |
| 原型设计稿 | https://www.figma.com/design/uqIdAMnXmbEUCrGxY2w4w0 |
一、项目简介与结对分工
1.1 项目背景与目标
校园卡、钥匙、耳机和雨伞等物品丢失或被捡到后,学生通常通过班级群、宿舍群或朋友圈发布信息。这些信息容易分散、被新消息覆盖,失主和拾物者也可能不在同一个群聊中。
本项目设计校园失物招领系统,将寻物和招领信息集中展示,帮助用户完成信息发布、搜索、详情查看、联系方式获取、状态修改等操作。项目同时关注手机端使用体验,通过统一的卡片布局、底部导航和状态颜色,让用户能够快速完成核心流程。
核心业务流程为:发布信息 → 浏览/搜索 → 查看详情 → 联系发布者 → 更新状态
1.2 结对分工
| 阶段 | 朱铮睿(102401625) | 林志涛(102401614) | 合作方式 |
|---|---|---|---|
| 第 1 阶段:需求分析 | 分析首页浏览、搜索、筛选和详情页需求 | 分析发布、我的发布和状态管理需求 | 共同阅读作业要求,确定用户角色和功能范围 |
| 第 2 阶段:数据与接口设计 | 设计数据结构与 localStorage 存储方式 |
确定页面需要的字段和接口 | 共同确定字段名称、ownerId 规则和模块接口 |
| 第 3 阶段:核心页面开发 | 完成首页、搜索、详情、发布页和数据层 | 完成状态语义统一、测试基线修复、README 整理 | 分别在功能分支开发,通过 GitHub PR 合并 |
| 第 4 阶段:功能完善 | 我的发布按发布者区分、搜索特殊字符修复、分类筛选 | 复制反馈修复、地点筛选、图片上传、54 个测试 | 在 PR 中互相审查,合并后回归测试 |
| 第 5 阶段:测试与修复 | 单元测试编写、数据层测试用例 | 状态相关测试用例、图片回归用例 | 共同使用 Chrome 测试,处理跨页面数据同步 |
| 第 6 阶段:协作提交 | 仓库维护、PR 合并、博客主体撰写 | PR 提交、README 更新、文档校对 | 共同完成 commit、PR、冲突处理、PSP 和博客 |
1.3 PSP 表格
| PSP2.1 阶段 | 中文说明 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 25 | 35 |
| Estimate | 估计任务所需时间 | 20 | 25 |
| Development | 开发阶段汇总 | 480 | 620 |
| Analysis | 需求分析(包括学习图片上传、localStorage 等技术) | 40 | 55 |
| Design Spec | 生成设计文档 | 30 | 35 |
| Design Review | 与队友复核字段、接口和页面跳转设计 | 25 | 30 |
| Coding Standard | 制定 HTML、CSS、JavaScript 和 Git 提交规范 | 15 | 15 |
| Design | 具体设计 | 40 | 50 |
| Coding | 编写数据层、首页、搜索、发布、详情、状态功能 | 220 | 300 |
| Code Review | 检查代码结构、文件范围和公共接口调用 | 40 | 50 |
| Test | 自测发布、搜索、状态修改和刷新流程 | 70 | 90 |
| Reporting | 编写报告和博客内容 | 80 | 90 |
| Test Report | 整理单元测试结果 | 40 | 45 |
| Size Measurement | 统计负责文件、功能点和提交规模 | 15 | 15 |
| Postmortem & Process Improvement Plan | 总结接口、合并和测试问题,提出改进措施 | 25 | 30 |
| 合计 | 690 | 855 |
实际耗时高于预估,主要增加在具体编码、测试和代码复审阶段。开发过程中需要多次进行 Git 分支同步、接口联调和异常排查,图片压缩、搜索特殊字符、PowerShell 执行策略等问题都占用了额外时间。并且由于两人工作时间的不同步,PR往往不能及时通过,导致两人的工作会陷入阶段停滞。以后需要先分工协商好,并理清开发流程。
二、解题思路描述与设计实现说明
2.1 代码实现思路
本项目采用纯前端方案:原生 HTML、CSS 和 JavaScript,数据存储在浏览器 localStorage 中,无需后端服务。这样做的原因是作业明确不要求复杂后台,且纯前端便于助教直接打开网页复现。
项目将数据处理和页面显示分离。js/data.js 作为公共数据模块,负责统一处理物品数据的读取、保存、搜索、状态更新和发布者校验。首页、搜索页、详情页、发布页和“我的发布”不直接维护数据,而是通过调用 data.js 的函数完成操作。
每条物品信息使用统一的数据结构,包括 id、type、name、category、location、date、description、contact、image、status、ownerId 和 createdAt 等字段。发布时间用 createdAt 保存绝对时间,页面显示时再格式化为“10月6日”等文字。
系统主要处理流程如下:
- 页面加载时从
localStorage读取数据; - 首页按类型筛选并按发布时间排序;
- 搜索页根据关键词、类型、分类、地点、状态进行多条件筛选;
- 点击卡片进入详情页,查看描述、地点、图片和联系方式;
- 发布者可以在“我的发布”中标记为已找到或已归还;
- 所有操作重新写入
localStorage,其他页面重新读取后自动显示最新结果。
2.2 关键流程图与数据流图
核心业务流程图:
数据流图:
我的发布 + 状态更新流程:
搜索筛选流程:
2.3 重要代码片段
片段一:数据层封装(js/data.js)
function addItem(item) {
const required = ['type', 'name', 'location', 'date', 'contact'];
for (const field of required) {
if (!item[field] || String(item[field]).trim() === '') {
throw new Error(`缺少必填字段:${field}`);
}
}
if (item.type !== 'lost' && item.type !== 'found') {
throw new Error('type 必须是 lost 或 found');
}
const newItem = {
id: generateId(),
type: item.type,
name: item.name.trim(),
category: item.category || '其他',
location: item.location.trim(),
date: item.date,
description: (item.description || '').trim(),
contact: item.contact.trim(),
status: 'active',
ownerId: getOwnerId(),
createdAt: Date.now()
};
const items = getAllItems();
items.unshift(newItem);
saveAllItems(items);
return newItem;
}
这段代码把“添加一条信息”的所有逻辑集中在一个函数里:校验必填字段、生成唯一 id、记录发布者、设置默认状态、写入 localStorage。页面调用时只需传入一个对象,不需要关心存储细节。
片段二:状态文案统一(js/data.js)
function getStatusText(type, status) {
if (type !== 'lost' && type !== 'found') return '状态未知';
if (status !== 'active' && status !== 'resolved') return '状态未知';
const labels = {
lost: { active: '寻找中', resolved: '已找到' },
found: { active: '待认领', resolved: '已归还' }
};
return labels[type][status];
}
存储层保持 active / resolved 两种状态,展示层根据信息类型分别显示“寻找中/已找到”和“待认领/已归还”,避免状态语义混乱。
片段三:搜索功能(js/data.js)
function searchItems(keyword) {
if (!keyword || keyword.trim() === '') return getAllItems();
const kw = keyword.trim().toLowerCase();
return getAllItems().filter(item => {
const name = (item.name || '').toLowerCase();
const desc = (item.description || '').toLowerCase();
return name.includes(kw) || desc.includes(kw);
});
}
搜索同时匹配名称和描述,并统一转小写,实现不区分大小写。
三、附加特点设计与展示
3.1 一键复制联系方式
设计意义:校园失物场景中,用户需要把 QQ 号发给对方,手动输入容易出错。一键复制减少操作步骤。
实现思路:优先使用 navigator.clipboard.writeText,不支持时降级为创建临时 textarea 并 document.execCommand('copy')。并且检查 execCommand 的返回值,失败时如实提示,不会“假成功”。
function fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
let ok = false;
try {
ok = document.execCommand('copy');
} catch (e) {
ok = false;
}
document.body.removeChild(textarea);
showToast(ok ? '已复制:' + text : '复制失败,请手动复制');
}
3.2 搜索历史记录
设计意义:用户搜索“校园卡”后,下次想再搜同样关键词,不需要重新输入。
实现思路:每次搜索时把关键词存入 localStorage,去重后最多保留 5 条。渲染历史时使用 data-index 属性配合事件监听,而不是内联 onclick 拼字符串,避免 O'Reilly 这类带单引号的关键词破坏 HTML 属性。
listEl.innerHTML = history.map((kw, index) => `
<div class="history-item" data-index="${index}">
<span class="icon">🕐</span>
${escapeHtml(kw)}
</div>
`).join('');
listEl.querySelectorAll('.history-item').forEach(el => {
el.addEventListener('click', function () {
const idx = parseInt(this.getAttribute('data-index'), 10);
const list = getHistory();
if (list[idx]) {
useHistory(list[idx]);
}
});
});
3.3 状态闭环展示
设计意义:发布者标记“已找到/已归还”后,其他用户应该能立刻看到该信息已解决,避免无效联系。
实现思路:首页、搜索结果页、详情页都调用 getStatusText(item.type, item.status) 获取统一文案;详情页在 resolved 状态下把联系按钮变为禁用的“该信息已解决”。
3.4 我的发布按发布者区分
设计意义:避免一个人修改别人发布的信息,保证数据归属清晰。
实现思路:addItem 时通过 getOwnerId() 生成并记录当前浏览器的用户标识,my-posts.js 只显示 ownerId 与当前用户匹配的记录。
function getOwnerId() {
let id = localStorage.getItem(OWNER_KEY);
if (!id) {
id = 'owner_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
localStorage.setItem(OWNER_KEY, id);
}
return id;
}
3.5 分类精确筛选 + 状态筛选
设计意义:首页分类入口改为按 category 字段精确过滤,搜索结果页新增状态筛选,用户能更快定位想要的信息。
实现思路:首页分类入口跳转 search-result.html?category=校园卡,结果页读取参数后按字段过滤;搜索页新增“全部 / 进行中 / 已解决”筛选按钮,参数通过 URL 传递。
3.6 单张物品图片上传
设计意义:失物招领场景中,图片比文字描述更直观,能帮助失主快速辨认物品。
实现思路:发布页支持选择 JPG/PNG/WebP 图片,用 Canvas 压缩到最长边 1000px,JPEG 质量按 0.78/0.60/0.45 分档,压缩后以 Data URL 形式存入 localStorage。首页、搜索结果页显示缩略图,详情页显示大图;无图片或解码失败时回退到分类图标。
成果展示:

3.7 地点关键词筛选 + 组合筛选
设计意义:用户可能只记得“在三区教学楼丢的”,不一定记得物品名称。地点筛选补上了这个场景。
实现思路:搜索页新增地点关键词输入,与物品关键词、类型、分类、状态按 AND 组合筛选,重新搜索时保留条件。
3.8 发布者归属校验
设计意义:避免用户误改他人发布的信息。
实现思路:每条信息记录 ownerId,“我的发布”只显示匹配当前浏览器 ownerId 的记录;无 ownerId 的旧记录保留浏览和搜索,但不自动认领。
四、项目结构与运行说明
4.1 项目目录结构
102401625-102401614/
├── index.html # 首页:信息浏览、类型筛选、分类快捷入口
├── publish.html # 发布信息页
├── publish-success.html # 发布成功页
├── search.html # 搜索页:关键词、类型/分类/状态筛选、搜索历史
├── search-result.html # 搜索结果页
├── detail.html # 信息详情页
├── contact.html # 联系发布者页:一键复制 QQ
├── my-posts.html # 我的发布页:进行中/已完成
├── status-updated.html # 状态更新成功页
├── package.json # 项目配置与测试脚本
├── README.md # 目录说明与使用说明
├── css/
│ └── style.css # 全局样式
├── js/
│ ├── data.js # 数据层:增删改查、搜索、状态更新、图片处理
│ ├── index.js # 首页逻辑
│ ├── publish.js # 发布页逻辑
│ ├── search.js # 搜索页逻辑
│ ├── detail.js # 详情页逻辑
│ ├── contact.js # 联系页逻辑
│ └── my-posts.js # 我的发布页逻辑
└── tests/
└── data.test.js # 单元测试(Mocha + Chai,54 个用例)
目录组织说明:
- 根目录 HTML 文件对应各个页面,
css/存放全局样式,js/按数据层和页面逻辑拆分; js/data.js是各页面共用的数据入口,所有增删改查、搜索、状态更新、图片处理都通过它完成;tests/data.test.js是单元测试文件,按作业要求不提交到仓库,但保留在本地供运行验证。
4.2 项目运行说明
项目使用原生 HTML、CSS 和 JavaScript 编写,没有引入 Vue、React 等第三方前端框架。网页本身不需要启动后端服务器,测试人员下载完整项目后,可以直接用 Chrome 或 Edge 打开 index.html。
运行网页:
- 从 GitHub 下载项目到本地;
- 用 Chrome 或 Edge 打开
index.html; - 无需安装任何依赖即可运行,
localStorage会自动保存数据。
运行单元测试:
npm ci
npm test
期望输出 54 passing。
注意:如果 PowerShell 提示“禁止运行脚本”,请运行
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned,或改用 CMD。
4.3 测试人员操作说明
- 打开首页,在“最新信息 / 寻物启事 / 招领信息”之间切换,点击分类图标浏览;
- 进入搜索页,输入物品名称,组合测试类型、分类、状态筛选;
- 打开任意物品详情,检查图片、地点、联系方式、复制按钮;
- 点击底部“发布”,选择“寻物/招领”,填写必填项,选择图片后发布;
- 进入“我的”,查看发布数量,测试“标记为已找到/已归还”;
- 刷新浏览器,确认发布、状态、图片等数据仍然保留。
4.4 本地数据与重置说明
项目数据保存在浏览器当前站点的 localStorage 中,主要包括物品信息、搜索历史等。清除浏览器数据后会丢失。如需恢复初始状态,可打开 Chrome 开发者工具,进入 Application → Local Storage → 清除对应站点数据。
五、单元测试
5.1 测试工具与学习过程
本项目使用 Mocha + Chai。Mocha 是测试框架,负责组织和运行测试用例;Chai 是断言库,提供 expect 语法。选择理由:轻量、配置简单、社区文档丰富,适合本次小型前端项目。
学习过程:阅读 Mocha 官方文档的 “Getting Started” 部分,了解 describe 和 it 的用法;阅读 Chai 的 expect API 文档。遇到的问题是 Node.js 环境没有 localStorage,解决方法是写一个 LocalStorageMock 类模拟浏览器存储行为。
5.2 部分测试代码
it('2. addItem 正常添加一条信息,getAllItems 长度变为 1', function () {
const item = data.addItem({
type: 'lost',
name: '校园卡',
category: '校园卡',
location: '三区教学楼',
date: '2026-10-05',
description: '蓝色卡套',
contact: '2766912385'
});
expect(item).to.have.property('id');
expect(item.status).to.equal('active');
expect(data.getAllItems()).to.have.lengthOf(1);
});
测试的函数是 addItem,验证它返回的对象包含 id、状态默认为 active,并且数据确实写入了存储。
it('13. lost + active 显示“寻找中”', function () {
expect(data.getStatusText('lost', 'active')).to.equal('寻找中');
});
测试的函数是 getStatusText,验证四种业务状态的文案映射正确。
5.3 测试数据构造思路
- 正常路径优先:先测“添加成功”“搜索命中”“更新成功”这些主流程。
- 边界情况:空存储、空关键词、缺少必填字段、非法 type。
- 异常路径:搜索不存在的关键词、更新不存在的 id、删除不存在的 id。
- 状态语义覆盖:四种 type/status 组合对应的文案,以及非法 type 和非法 status 的兜底显示。
- 图片回归用例:无图发布、有图发布、压缩后 Data URL 长度、超容量处理等。
- 大小写与部分匹配:搜索
airpods能匹配AirPods,搜索“深蓝色”能匹配描述。
针对将来测试人员的“刁难”,我们考虑了:传入 null 或 undefined 作为关键词、传入超长字符串、重复添加同一条信息、图片处理失败等场景。
5.4 测试结果
最终测试结果:

在原有 43 个测试基础上,新增 11 个图片回归用例。测试范围覆盖数据存储、表单校验、信息发布、搜索筛选、详情查询、我的发布、状态修改、权限判断和异常处理。
六、GitHub 协作与代码签入
6.1 协作方式
项目开发过程中,我们没有两个人直接同时修改 main,而是采用:
最新 main → 创建 feature 分支 → 开发 → Commit → Push → Pull Request → 对方检查 → Merge
林志涛 fork 仓库后提交了两次 Pull Request:
-
PR #1:
feat: 完善测试基线并统一失物招领状态,修复 Mocha/Chai 依赖锁文件,统一四种业务状态,新增 8 个状态相关测试,共 20 个测试通过。
![image]()
-
PR #2:
feat: 完善复制与状态校验,支持地点筛选和单张物品图片,新增发布者归属校验、地点筛选、图片上传,测试达到 54 passing。
![image]()
Commit:


6.2 遇到的问题与解决方法
问题一:首页缺少状态标签
- 问题描述:在“我的发布”中把信息标记为已解决后,回到首页,卡片上只显示“寻物/招领”,没有状态标签。
- 做过哪些尝试:检查
renderList()函数,发现只渲染了类型标签,没有读取status字段。 - 是否解决:已解决,在卡片模板中通过
getStatusText增加状态标签。 - 有何收获:多个页面共用同一份数据时,状态展示要统一检查,不能只改一个页面。
问题二:单元测试 0 passing
- 问题描述:运行
npm test显示0 passing,测试文件没有被执行。 - 做过哪些尝试:检查
package.json的 test 脚本,发现tests/**/*.test.js在 Windows CMD 下不被正确解析;同时发现tests/data.test.js是 0 字节空文件。 - 是否解决:已解决,把 test 脚本改为
mocha tests/*.test.js,并重新粘贴测试代码。 - 有何收获:Windows 下的 glob 通配符行为和 Linux 有差异,写 npm script 时要考虑兼容性。
问题三:搜索历史特殊字符 Bug
- 问题描述:搜索历史里出现
O'Reilly这种带单引号的内容,点击时可能直接把 JavaScript 弄报错。 - 做过哪些尝试:检查
renderHistory函数,发现用内联onclick拼关键词,单引号会破坏 HTML 属性。 - 是否解决:已解决,改用
data-index属性配合事件监听,关键词只出现在文本内容中。 - 有何收获:任何时候都不要把用户输入直接拼进 HTML 属性里。
问题四:图片上传的压缩与存储
- 问题描述:原图可能达到几 MB,直接存入 localStorage 会超容量。
- 做过哪些尝试:用 Canvas 压缩,按最长边 1000px、JPEG 质量分档处理。
- 是否解决:已解决,压缩后 Data URL 控制在 524288 字符以内,超容量时提示原因并保留表单。
- 有何收获:前端存储有容量限制,图片必须先压缩再存。
七、评价队友
7.1 值得学习的地方
朱铮睿在项目整体框架搭建和基础功能实现上完成得比较扎实,为后续功能完善提供了稳定的基础。在开发过程中,他能够根据作业要求及时补充分类筛选、搜索和“我的发布”等功能,并在我提交 PR 后认真检查代码、及时合并修改,使两人的开发进度能够顺利衔接。同时,他对项目整体页面结构和功能流程比较熟悉,在后续调整中也能够较快定位需要修改的位置,这一点值得我学习。
7.2 需要改进的地方
后续如果还有结对编程任务,希望我们可以在正式编码前进一步明确功能划分、数据结构和接口约定,并增加开发过程中的实时沟通。这样可以减少重复修改和后期合并时的协调成本,也能让两个人的开发节奏更加统一。
八、个人总结
林志涛:我在本次作业中主要负责状态语义统一、测试基线修复、地点筛选、图片上传和文档整理。通过这次任务我发现,同一个 resolved 状态在寻物和招领场景下的含义完全不同,寻物叫“已找到”,招领叫“已归还”,如果不在展示层做区分,用户会感到困惑。图片上传这个功能让我第一次认真考虑了前端存储的容量限制——原图几 MB 直接存 localStorage 会爆,必须用 Canvas 压缩到合理尺寸再存,还要处理透明区域、损坏图片、超容量等异常情况。这让我理解了“功能能跑通”和“功能可靠”之间的差距。不足的是我在前端交互上参与较少,后续会加强 JavaScript 的实际练习。



浙公网安备 33010602011771号