2026秋软件工程第二次结对作业
找呀找——校园失物招领 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 协作流程与开发记录
由 102402107 创建主仓库,102402108 Fork 后在独立分支开发。每完成一轮功能,开发者提交 Commit、Push 并发起 Pull Request;检查修改后合并到 main,下一轮开始前同步最新代码。
项目按照 搜索 → 详情 → 发布 → 状态管理 → UI 优化 → 图片功能 → 测试与文档 的顺序逐步迭代。以下为 GitHub 代码签入记录截图。
(1)前期核心功能开发记录
(2)后期功能完善与项目收尾记录
三、PSP 表格与分析
3.1 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 |
3.2 PSP 分析
根据记录,项目预估耗时 1140 分钟,实际耗时 1660 分钟,增加 520 分钟(约 45.6%)。其中编码增加 230 分钟,主要来自图片处理和多轮 UI 调整;测试增加 95 分钟,用于功能走查、修复问题和编写自动化测试。GitHub 分支同步、PR 与账号认证也需要学习和排查。
这次经历说明,项目计划不能只估计首次编码时间,还应为测试、界面返工和协作预留缓冲。
四、解题思路与架构设计
4.1 模块划分
项目将页面结构、业务逻辑和交互分开:
| 模块 | 文件 | 主要职责 |
|---|---|---|
| 页面与样式 | index.html、styles.css、overrides.css |
页面骨架、粉色视觉、横屏布局与响应式样式 |
| 业务逻辑 | assets/js/core.js |
搜索筛选、表单校验、创建信息和状态切换 |
| 页面交互 | assets/js/app.js |
Hash 导航、DOM 渲染、图片处理和 localStorage 读写 |
网页通过 location.hash 切换首页、搜索、发布、详情、我的发布和历史记录等视图,无需启动服务器。数据仅存储在当前浏览器,并不跨设备同步。
4.2 业务流程图
下图展示发布、浏览/搜索、详情查看及状态更新流程,包含表单校验失败、搜索无结果和保存失败等分支。物品完成后从首页进行中列表移出,但仍可在历史记录中查看。
五、核心功能实现与关键代码
5.1 搜索与筛选
core.js 将关键词、信息类型、物品分类、地点和状态统一交给 filterItems() 处理。一条信息需要满足全部已启用条件才能出现在结果中;若无匹配项,页面显示空结果提示。
核心代码如下:
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;
});
}
例如用户同时选择“寻物 + 数码电子 + 图书馆”,函数会逐条排除不满足条件的信息。这样可以复用同一套逻辑实现首页、搜索和历史记录的不同展示。
5.2 发布与表单校验
寻物与招领共用发布表单,按类型切换“丢失时间/地点”和“拾取时间/地点”。在创建记录前,validateItem() 检查必填字段、描述长度及手机号/邮箱格式,失败时返回字段级错误而不是直接发布。
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
};
}
例如联系方式为 123 时,函数会返回 errors.contact,页面据此提示用户修改。
5.3 状态管理
寻物状态为“寻找中 ↔ 已找到”,招领状态为“等待认领 ↔ 已归还”。状态切换前先确认,完成后同步保存;再次渲染时,首页、详情、我的发布和历史记录读取的是同一份数据。
function toggleStatus(item) {
if (
!item ||
!["lost", "found"].includes(item.type)
) {
return item;
}
return Object.assign({}, item, {
status: statusFor(
item.type,
!isCompleted(item)
)
});
}
5.4 本地数据保存
项目使用 localStorage 持久化数据。保存前检查序列化数据大小;如果存储失败,提示用户并阻止把失败操作当作发布成功。
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;
}
}
这种方案免去服务器配置,适合课程演示;不足是数据受浏览器容量限制,也不能跨设备共享。
六、附加特点设计与展示
6.1 真实照片与多图预览
设计意义: 同类物品外观容易相似,照片可辅助失主识别细节,减少误认。
实现思路: 照片可不上传,最多 4 张;单张原图限制 2MB,用户可以预览、删除并重新选择。详情页按数量排列图片,支持放大查看。为了降低 localStorage 压力,在浏览器端使用 Canvas 缩小尺寸并压缩为 JPEG。
关键压缩代码节选:
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
);
}
代码按原图宽高计算缩放比例,使最长边不超过 900 像素,再逐步降低 JPEG 质量,减少保存的数据量。
成果展示:
6.2 多条件组合筛选
设计意义: 用户有时只记得地点或物品类别,单纯关键词搜索难以快速缩小范围。项目增加寻物/招领类型、八类物品、地点和状态组合筛选。
实现思路: 不同条件都通过第五节的 filterItems() 进行判断,避免多个页面分别实现筛选规则。八类物品为:证件卡类、数码电子、学习用品、生活用品、衣物配饰、钥匙、雨具、其他。
例如可筛选:寻物 + 数码电子 + 图书馆 + 进行中。对应筛选逻辑和效果已在第五节展示。
6.3 已完成信息归档
设计意义: 已找到、已归还的信息继续占据首页,会增加无效浏览与重复联系。
实现思路: 首页只展示进行中信息,已完成记录保留在历史记录,不直接删除;必要时可以在“我的发布”恢复为进行中。
关键代码:
const activeItems = Core.filterItems(
items,
{ status: "active" }
).sort(
(a, b) => b.createdAt - a.createdAt
);
在状态更新后重新执行筛选,记录便会进入对应列表。
6.4 其他便捷功能
详情页提供一键复制联系方式,并准备兼容复制方式;“我的发布”支持删除错误信息和确认操作;右上角的恢复示例数据便于课程演示与重复测试。
七、页面效果展示
页面采用统一的简约粉色风格,使用圆角卡片、柔和渐变和固定底部导航。以下以最终页面截图展示主要布局,具体业务逻辑不再重复说明。
首页: 左侧搜索与快捷发布,右侧进行中信息列表独立滚动。
搜索页: 筛选条件保持可见,结果区独立滚动。
发布页: 寻物/招领共用表单,照片为可选项。
详情页: 展示状态、物品信息、联系方式与照片。
我的发布: 可查看和维护当前浏览器中的个人发布。
八、项目目录与运行说明
8.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 # 使用说明
8.2 运行方法
- 打开 GitHub 主仓库。
- 点击 Code → Download ZIP,解压后进入项目目录。
- 用 Google Chrome 直接打开
index.html,无需启动服务器或安装前端依赖。 - 可以依次测试“发布 → 搜索 → 详情 → 联系 → 我的发布中更新状态 → 历史记录”;需要重复演示时使用页面右上角恢复按钮。
项目数据保存在当前浏览器 localStorage 中,刷新后仍可保留;但清除浏览器数据或换设备不会同步。更多说明见 README.md。
九、单元测试与测试分析
9.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
9.2 白盒测试与测试数据设计
根据 core.js 的条件分支设计测试,兼顾正常、异常和边界情况。搜索测试覆盖命中/不命中、单条件/组合条件;发布校验覆盖空字段、描述过短、非法联系方式、合法手机号与邮箱;状态测试覆盖寻物与招领的双向状态;图片测试覆盖 0、1、4、5 张及旧单图数据兼容。
共编写 25 个自动化单元测试,主要分布如下:
| 编号 | 测试范围 | 主要检查 |
|---|---|---|
| 01–06 | 文本与搜索 | 标准化、关键词、类型、分类、条件冲突 |
| 07–11 | 发布校验 | 必填项、描述长度、手机号和邮箱 |
| 12–16 | 信息与状态 | 默认状态、状态切换、分类去重 |
| 17–19 | 状态筛选 | 进行中、已完成及组合查询 |
| 20–25 | 图片边界 | 旧字段兼容、0/1/4/5 张、非法数据 |
9.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]
);
}
);
9.4 测试结果与评价
自动化单元测试共 25 项,通过 25 项,失败 0 项。另外在 Chrome 中按照 docs/MANUAL_TESTS.md 进行 25 项人工走查,覆盖发布、搜索无结果、复制联系方式、状态更新、图片预览、存储与不同分辨率布局等场景。
我们重点考虑测试人员可能遇到的异常输入、图片超限、刷新后数据是否保留、直接通过 file:// 打开网页等情况。单元测试主要验证纯逻辑,人工测试补充页面交互;目前尚未量化代码分支覆盖率,也未实现完整的浏览器端自动化测试。
十、开发中遇到的问题及解决方法
10.1 页面布局与原型不一致
问题: 首页比例、按钮位置与信息列表滚动效果需要反复调整。
尝试: 检查 HTML 容器关系,在 Chrome 中逐轮调整 CSS 布局和高度,并使用 overrides.css 集中处理。
结果: 最终形成较稳定的粉色双栏与局部滚动布局。
收获: 页面不仅要好看,也要在真实交互和不同窗口尺寸下可用。
10.2 图片占用本地存储空间
问题: 多张原始图片容易达到 localStorage 容量上限。
尝试: 限制单图 2MB、最多 4 张,使用 Canvas 缩放压缩,并处理保存失败提示。
结果: 降低了存储压力,避免保存失败却显示成功。
收获: 新功能需要同时设计异常与失败处理。
十一、对队友的评价
评价对象:102402108 阿合加依娜尔
值得学习的地方: 队友负责了搜索筛选、信息发布和首页优化等重要功能,并参与最终 UI 与文档收尾。她能够按照分工推进任务,通过 Fork 和 PR 提交修改。多轮开发让我体会到及时沟通和同步代码的重要性。
需要改进的地方: 后续可以在每轮开发前更明确地约定验收标准,包括页面效果、边界测试和文档更新范围,减少最后阶段重复检查与返工。
十二、个人总结与反思
通过这次结对作业,我真正经历了从原型到可运行网页的过程。原本看起来简单的发布和图片上传,实际还需要处理表单校验、数据存储、压缩、数量限制和失败提示;页面布局也要在多轮修改中检查滚动、按钮与信息层次。
这次我还实际使用了 GitHub 的 Branch、Commit、Fork 和 Pull Request,学会了分支同步、合并及排查忽略文件问题。编写单元测试让我认识到,将 core.js 中的业务逻辑与页面交互分开,能够让搜索、校验和状态更新更容易验证。
项目目前仍有局限:localStorage 不能跨设备共享,图片数量受容量限制,也没有真实账号认证。若继续开发,我会优先考虑数据存储和身份管理,并引入浏览器端自动化测试,而不是盲目增加功能。
本次最大的收获是:完成一个软件不仅是把功能写出来,更需要持续测试、修改和与队友协作。
附:项目相关链接
GitHub 主仓库:
https://github.com/cccch412/102402107-102402108
102402108 Fork 仓库:
https://github.com/ahejiayinaer/102402107-102402108
项目使用说明:
GitHub 协作说明:
单元测试报告:
人工测试记录:
自动化单元测试代码:
浙公网安备 33010602011771号