第二次结对作业
找呀找——校园失物招领 WEB 应用
| 项目 | 内容 |
|---|---|
| 本次作业要求 | 2026 秋软件工程第二次结对作业之程序实现 |
| 所属课程 | 2026 秋软件工程与软件工程实践 |
| 结对成员 | 102402107 陈瑀涵、102402108 阿合加依娜尔 |
| 107 个人博客 | 陈瑀涵的博客 |
| 108 个人博客 | 阿合加依娜尔的博客 |
| GitHub 主仓库 | 102402107-102402108 |
| GitHub Fork 仓库 | 102402108 的 Fork |
| 实现形式 | PC Web(HTML、CSS、JavaScript) |
| 测试环境 | Google Chrome、Node.js 内置测试工具 |
一、项目背景与需求分析
1.1 项目背景
校园失物招领信息常分散在班级群、宿舍群等渠道,消息容易被覆盖,失主查找不便,拾取者也难以联系失主。在第一次结对作业中,我们完成了“找呀找”的需求分析和原型设计,本次作业将原型实现为可直接运行的 WEB 应用。
1.2 核心需求与范围
围绕 “发布信息 → 浏览或搜索 → 查看详情 → 联系发布者 → 更新状态” 这条主线,项目支持发布寻物或招领信息、关键词搜索、查看物品详情与联系方式,以及由发布者标记“已找到/已归还”。
我们选用原生 HTML、CSS 与 JavaScript,确保下载后可通过 Chrome 直接打开 index.html。不涉及复杂后台、实名认证、即时聊天或地图定位,而是在保障核心流程可用的前提下,加入照片上传、组合筛选与历史记录等便捷功能。
二、分工与协作过程
2.1 分工明细
| 成员 | 主要工作 |
|---|---|
| 102402107 陈瑀涵 | 创建和维护 GitHub 主仓库;详情页与联系方式复制、我的发布与状态更新、粉色 UI 重构、图片上传及页面交互;检查合并 PR、整理最终材料 |
| 102402108 阿合加依娜尔 | Fork 项目;关键词搜索与筛选、寻物和招领发布、首页信息列表优化;最终 UI 细节调整和测试文档整理 |
| 共同完成 | 功能讨论、页面走查、异常修复、版本同步与测试 |
2.2 协作流程与开发过程
项目开展路线:搜索 → 详情 → 发布 → 状态管理 → UI 优化 → 图片功能 → 测试与文档 ,以下为 GitHub 代码签入记录截图
(1)核心功能开发(前期)

(2)功能完善与补充(后期)

三、PSP表格及分析
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Estimate | 估计这个任务需要多少时间 | 10 | 10 |
| Analysis | 需求分析(包括学习新技术) | 90 | 130 |
| Design Spec | 生成设计文档 | 60 | 75 |
| Design Review | 设计复审 | 30 | 45 |
| Coding Standard | 代码规范 | 25 | 30 |
| Design | 具体设计 | 100 | 140 |
| Coding | 具体编码 | 480 | 710 |
| Code Review | 代码复审 | 90 | 130 |
| Test | 测试 | 150 | 245 |
| Test Report | 测试报告 | 30 | 40 |
| Size Measurement | 计算工作量 | 30 | 40 |
| Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 45 | 65 |
| 合计 | 1140 | 1660 |
分析结果:根据记录,项目预估耗时 1140 分钟,实际耗时 1660 分钟,增加了 520 分钟(约 45.6%)。其中编码环节增加 230 分钟,主要源于图片处理与多轮 UI 调整;测试环节增加 95 分钟,用于功能走查、问题修复及自动化测试编写。此外,GitHub 分支同步、PR 流程与账号认证也需要额外学习与排查。这次经历表明,项目计划不能只估算首次编码时间,还应为测试、界面返工和团队协作预留缓冲。
四、解题思路描述与设计实现说明
4.1代码实现思路
(1)模块划分——结构、逻辑、交互”三层分离
| 模块 | 文件 | 主要职责 |
|---|---|---|
| 页面与样式 | index.html、styles.css、overrides.css |
负责页面骨架、粉色视觉风格、横屏布局与响应式样式 |
| 业务逻辑 | assets/js/core.js |
负责搜索筛选、表单校验、信息创建与状态切换等纯逻辑处理 |
| 页面交互 | assets/js/app.js |
负责 Hash 导航、DOM 渲染、图片处理与 localStorage 读写 |
(2)基于 Hash 的视图切换
通过 location.hash 在首页、搜索、发布、详情、我的发布和历史记录等视图之间切换,无需启动服务器。
4.2 业务流程图

4.3 核心功能及关键代码
(1)发布及内容校验
关键代码:
function validateItem(draft) {
const errors = {};
if (!draft || !["lost", "found"].includes(draft.type)) errors.type = "请选择信息类型";
if (!normalize(draft && draft.title)) errors.title = "请填写物品名称";
if (!normalize(draft && draft.category)) errors.category = "请选择物品分类";
if (!normalize(draft && draft.date)) errors.date = "请选择时间";
if (!normalize(draft && draft.location)) errors.location = "请填写地点";
if (normalize(draft && draft.description).length < 5) errors.description = "描述至少填写 5 个字";
if (!/^1\d{10}$/.test(normalize(draft && draft.contact)) && !/^[\w.-]+@[\w.-]+\.[a-z]{2,}$/i.test(normalize(draft && draft.contact))) {
errors.contact = "请填写 11 位手机号或有效邮箱";
}
return { valid: Object.keys(errors).length === 0, errors };
}
当没有填写地址时,系统发出提示

完成填写内容校验,通过后可发布

(2)检索与筛选
关键代码:
function filterItems(items, filters) {
const source = Array.isArray(items) ? items : [];
const query = normalize(filters && filters.query);
const type = normalize(filters && filters.type);
const category = normalize(filters && filters.category);
const location = normalize(filters && filters.location);
const status = normalize(filters && filters.status);
return source.filter((item) => {
if (query && !searchableText(item).includes(query)) return false;
if (type && type !== "all" && item.type !== type) return false;
if (category && category !== "all" && normalize(item.category) !== category) return false;
if (location && !normalize(item.location).includes(location)) return false;
if (status === "active" && isCompleted(item)) return false;
if (status === "done" && !isCompleted(item)) return false;
return true;
});
}
系统会根据用户输入的关键词筛选并列出符合条件的物品

(3)状态更新
关键代码:
function toggleStatus(item) {
if (!item || !["lost", "found"].includes(item.type)) return item;
return Object.assign({}, item, { status: statusFor(item.type, !isCompleted(item)) });
}
function uniqueCategories(items) {
return Array.from(new Set((items || []).map((item) => item.category).filter(Boolean))).sort();
}
return { normalize, createId, statusFor, isCompleted, filterItems, validateItem, buildItem, toggleStatus, uniqueCategories };
});
寻物和招领的两种状态可以来回切,切之前先弹窗确认,确认后马上存进本地,所有页面共用同一份数据,改一处、处处同步
(4)数据存储
关键代码:
function saveItems() {
try {
const serialized = JSON.stringify(items);
if (serialized.length > MAX_STORAGE_CHARS) {
throw new Error("storage-limit");
}
localStorage.setItem(
STORAGE_KEY,
serialized
);
return true;
} catch (_) {
showToast(
"本地存储空间不足,请减少照片数量或更换较小的照片"
);
return false;
}
}
五、附加特点设计与展示
5.1图片上传与本地预览
(1)设计意义:同类物品外观容易相似,照片可辅助失主识别细节,减少误认
(2)实现思路:如上传照片,最多支持 4 张;单张原图限制 2MB,用户可以预览、删除并重新选择。详情页按数量排列图片,支持放大查看。为了降低 localStorage 压力,在浏览器端使用 Canvas 缩小尺寸并压缩为 JPEG
(3)关键代码:
const maxSide = 900;
const scale = Math.min(
1,
maxSide / Math.max(
photo.width,
photo.height
)
);
const canvas = document.createElement("canvas");
canvas.width = Math.max(
1,
Math.round(photo.width * scale)
);
canvas.height = Math.max(
1,
Math.round(photo.height * scale)
);
canvas.getContext("2d").drawImage(
photo,
0,
0,
canvas.width,
canvas.height
);
let quality = 0.76;
let compressed = canvas.toDataURL(
"image/jpeg",
quality
);
while (
compressed.length > 900000 &&
quality > 0.44
) {
quality -= 0.08;
compressed = canvas.toDataURL(
"image/jpeg",
quality
);
}
效果展示:

5.2 多条件组合筛选
(1)设计意义: 用户有时只记得地点或物品类别,单纯关键词搜索难以快速缩小范围。项目增加寻物/招领类型、八类物品、地点和状态组合筛选
(2)实现思路: 不同条件都通过第四节的检索与筛选进行判断,避免多个页面分别实现筛选规则。八类物品为:证件卡类、数码电子、学习用品、生活用品、衣物配饰、钥匙、雨具、其他
例如可筛选:寻物 + 帽子 + 图书馆,对应筛选逻辑和效果已在第四节展示
5.3已完成信息归档
(1)设计意义: 已找到、已归还的信息继续占据首页,会增加无效浏览与重复联系
(2)实现思路: 首页只展示进行中信息,已完成记录保留在历史记录,不直接删除;必要时可以在“我的发布”恢复为进行中
(3)关键代码:
const activeItems = Core.filterItems(
items,
{ status: "active" }
).sort(
(a, b) => b.createdAt - a.createdAt
);
效果展示:

六、完整项目展示
依次为:首页、详情页、搜索页、发布页和个人主页

七、项目目录与运行说明
7.1 目录结构
102402107-102402108/
index.html # 网页入口
assets/
css/
styles.css # 基础样式
overrides.css # 横屏与交互细节样式
js/
core.js # 核心业务逻辑
app.js # 页面交互及存储
docs/
GIT_GUIDE.md # GitHub 协作说明
MANUAL_TESTS.md # 人工测试记录
TEST_REPORT.md # 测试报告
tests/
core.test.js # 25 个单元测试
package.json # npm test 命令配置
.gitignore
README.md # 使用说明``
7.2运行方法
(1)打开Github主仓库: https://github.com/cccch412/102402107-102402108 。
(2)点击 Code → Download ZIP,解压后进入项目目录。
(3)用Google Chrome直接打开 index.html,无需启动服务器或安装前端依赖。
(4)可以依次测试“发布 → 搜索 → 详情 → 联系 → 我的发布中更新状态 → 历史记录”;需要重复演示时使用页面右上角恢复按钮。
项目数据保存在当前浏览器 localStorage中,刷新后仍可保留;但清除浏览器数据或换设备不会同步。更多说明见 README.md。
八、单元测试与测试分析
8.1 测试工具及简易教程
使用 Node.js 内置 node:test 和 node:assert/strict,无需额外安装测试框架。基本过程是:引入待测模块、通过 test() 定义用例,再用 assert.equal() 或 assert.deepEqual() 比较实际与预期结果。
const test = require("node:test");
const assert = require("node:assert/strict");
const Core = require("../assets/js/core.js");
test("文本标准化", () => {
assert.equal(Core.normalize(" AbC "), "abc");
});
安装 Node.js 后,在根目录执行:
npm test
也可以直接运行:
node --test tests/core.test.js
8.2 白盒测试与测试数据设计
根据 core.js 的条件分支设计测试,兼顾正常、异常和边界情况。搜索测试覆盖命中/不命中、单条件/组合条件;发布校验覆盖空字段、描述过短、非法联系方式、合法手机号与邮箱;状态测试覆盖寻物与招领的双向状态;图片测试覆盖 0、1、4、5 张及旧单图数据兼容。
共编写 25 个自动化单元测试,主要分布如下:
| 编号 | 测试范围 | 主要检查 |
|---|---|---|
| 01–06 | 文本与搜索 | 标准化、关键词、类型、分类、条件冲突 |
| 07–11 | 发布校验 | 必填项、描述长度、手机号和邮箱 |
| 12–16 | 信息与状态 | 默认状态、状态切换、分类去重 |
| 17–19 | 状态筛选 | 进行中、已完成及组合查询 |
| 20–25 | 图片边界 | 旧字段兼容、0/1/4/5 张、非法数据 |
8.3 关键测试代码
组合条件冲突: 测试不存在的组合应返回空数组。
test(
"06 组合条件无结果时返回空数组",
() => assert.deepEqual(
Core.filterItems(items, {
type: "lost",
category: "雨具"
}),
[]
)
);
非法联系方式: 输入 123 应被校验函数拒绝。
test(
"09 非法联系方式会被拒绝",
() => {
const r = Core.validateItem({
type: "lost",
title: "耳机",
category: "数码",
date: "2026-10-01",
location: "图书馆",
description: "遗失白色耳机",
contact: "123"
});
assert.ok(r.errors.contact);
}
);
图片上限: 给出 5 张图片时只能保存前 4 张。
test(
"21 新建信息最多保存四张真实照片",
() => {
const images = Array.from(
{ length: 5 },
(_, i) => `data:image/jpeg;base64,${i}`
);
const r = Core.buildItem({
type: "lost",
title: "耳机",
category: "数码电子",
date: "2026-10-01",
location: "图书馆",
description: "遗失白色耳机",
contact: "13800138000",
images
}, 1003);
assert.equal(
r.item.images.length,
4
);
assert.equal(
r.item.image,
images[0]
);
}
);
8.4 测试结果与评价
自动化单元测试共 25 项,通过 25 项,失败 0 项。另外在 Chrome 中按照 docs/MANUAL_TESTS.md 进行 25 项人工走查,覆盖发布、搜索无结果、复制联系方式、状态更新、图片预览、存储与不同分辨率布局等场景。
我们重点考虑测试人员可能遇到的异常输入、图片超限、刷新后数据是否保留、直接通过 file:// 打开网页等情况。单元测试主要验证纯逻辑,人工测试补充页面交互;目前尚未量化代码分支覆盖率,也未实现完整的浏览器端自动化测试。
九、开发中遇到的问题及解决方法
9.1 页面布局与原型不一致
问题: 首页比例、按钮位置与信息列表滚动效果需要反复调整。
尝试: 检查 HTML 容器关系,在 Chrome 中逐轮调整 CSS 布局和高度,并使用 overrides.css 集中处理。
结果: 最终形成较稳定的粉色双栏与局部滚动布局。
收获: 页面不仅要好看,也要在真实交互和不同窗口尺寸下可用。
9.2 图片占用本地存储空间
问题: 多张原始图片容易达到 localStorage 容量上限。
尝试: 限制单图 2MB、最多 4 张,使用 Canvas 缩放压缩,并处理保存失败提示。
结果: 降低了存储压力,避免保存失败却显示成功。
收获: 新功能需要同时设计异常与失败处理。
十、对队友的评价
评价对象:102402107 陈瑀涵
值得学习的地方: 在本次项目中,陈瑀涵承担了主仓库维护、Pull Request 合并,以及详情、状态管理、图片处理和多轮 UI 调整等工作。
她在页面细节和功能完整性方面投入了较多精力,能够在已有版本基础上持续发现问题并进行完善。
项目开发过程中,也推动了 README、测试记录和代码管理文档的整理,使最终成果不仅包含网页,还包含相对完整的使用和测试说明。
需要改进的地方: 后续可以进一步加强开发计划的阶段性安排,避免部分 UI 细节集中在开发后期反复修改。
同时,在新增较复杂的功能前,可以先明确数据结构、异常场景和测试方法,减少功能完成后的补充调整。
十一、个人总结与反思
这次结对编程让我更加熟悉了从原型设计到网页实现的过程。在搜索、发布和首页功能开发中,我体会到需要先理解用户真正要完成的操作,再组织页面元素和事件逻辑。例如,搜索功能不只是输入关键词,还要考虑类型、分类、地点和状态等条件如何组合;发布功能不只是展示表单,还需要处理错误输入以及发布后的页面反馈。通过多轮页面调整,我也认识到功能实现和用户体验需要同时考虑。一个功能即使能正常执行,如果入口不明显、提示不清楚或者页面布局不合理,也会影响最终使用效果。在 GitHub 协作方面,我学习了 Fork、功能分支、Commit、Push 和 Pull Request 的实际操作,也理解了每轮开发前同步最新主分支的重要性。
这次项目使我更加认识到结对开发需要合理分工、及时沟通和共同检查,而不是简单地把任务分成两半。今后如果继续进行类似项目,我希望能够更早确定功能验收标准,在开发过程中同步完成测试和文档整理,减少最后阶段的集中收尾工作。
附:项目相关链接
GitHub 主仓库:
https://github.com/cccch412/102402107-102402108
102402108 Fork 仓库:
https://github.com/ahejiayinaer/102402107-102402108
项目使用说明:
GitHub 协作说明:
单元测试报告:
人工测试记录:
自动化单元测试代码:
浙公网安备 33010602011771号