2026秋软件工程结对作业(第二次)
| 这个作业属于哪个课程 | H202601软件工程与软件工程实践 |
|---|---|
| 这个作业要求在哪里 | 2026秋软件工程结对作业(第二次之程序实现) |
| 这个作业的目标 | 将校园失物招领原型实现为可运行的 Web 应用,完成核心业务流程,并通过 GitHub 协作、PSP 和单元测试实践结对开发、版本管理与软件测试。 |
| GitHub 仓库 | 102401209-102401125 |
| 结对成员 | 102401209(洪添军)、102401125(罗宇泽) |
一、项目简介
本次结对作业是在第一次原型设计作业的基础上,将“校园失物招领”原型进一步实现为可以实际运行的 Web 应用。
项目围绕校园中常见的失物招领场景,实现以下核心业务流程:
发布信息 → 浏览/搜索 → 查看详情 → 联系发布者 → 查看我的发布 → 修改信息状态
本项目采用 PC Web 的形式实现,开发与测试统一使用 Google Chrome。
网页本身不需要启动后端服务器,也不依赖 npm 才能运行。测试人员下载完整项目后,可以直接使用 Google Chrome 打开 index.html。
以下为首页页面展示:

开发与测试环境
| 项目 | 环境 |
|---|---|
| 操作系统 | Windows |
| 浏览器 | Google Chrome 154.0.8037.98(64 位) |
| 前端技术 | HTML5、CSS3、原生 JavaScript |
| 数据存储 | localStorage |
| 页面路由 | Hash 路由 |
| 单元测试框架 | Vitest 5.0.3 |
| 版本管理 | Git / GitHub |
二、具体分工
为了减少两个人同时修改同一个文件造成的冲突,我们在开发前对任务进行了划分。
1. 我的主要工作
我主要负责项目的页面实现和用户交互,具体包括:
- 根据 Figma 原型完成网页界面;
- 首页卡片和滚动布局;
- 发布寻物页面;
- 发布招领页面;
- 搜索页面交互;
- 详情页面;
- 联系方式展示;
- 一键复制联系方式;
- “我的”和“我的发布”页面;
- 修改状态入口及确认框;
- 表单错误提示;
- 成功、失败和空结果等页面反馈;
- CSS 样式和页面视觉优化;
- 页面截图和最终界面检查。
2. 队友的主要工作
队友主要负责项目的数据层、业务逻辑和单元测试,具体包括:
- 设计失物招领信息统一数据结构;
- 实现
localStorage数据初始化、读取和保存; - 实现首页信息读取与排序;
- 实现关键词搜索;
- 实现寻物 / 招领类型筛选;
- 实现物品类别组合筛选;
- 为发布页面提供新增和保存接口;
- 为详情页面提供按 ID 查询接口;
- 为“我的发布”提供按发布者查询接口;
- 实现状态修改逻辑;
- 实现发布者权限判断;
- 保证首页、搜索、详情、“我的发布”之间的数据同步;
- 配置 Vitest;
- 编写搜索、存储、状态修改等单元测试;
- 参与 Git 分支、Pull Request 和项目联调。
3. 共同完成
我们共同完成:
- 前期需求分析;
- 数据字段和接口约定;
- Pull Request 互相检查;
- Chrome 页面联调;
- GitHub 协作;
- 最终功能测试;
- README、博客及提交材料检查。
三、PSP 表格
结合本次项目实际开发过程,我对各阶段耗时进行了整理。由于开发过程中包含需求讨论、Git 协作、接口联调、单元测试以及问题排查,因此实际耗时相比最初预估略有增加。
| PSP 阶段 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|
| Planning 计划 | 30 | 45 |
| Estimate 估计任务时间 | 30 | 20 |
| Analysis 需求分析 | 40 | 90 |
| Design Spec 生成设计文档 | 50 | 80 |
| Design Review 设计复审 | 60 | 80 |
| Coding Standard 代码规范 | 20 | 30 |
| Design 具体设计 | 80 | 120 |
| Coding 具体编码 | 420 | 450 |
| Code Review 代码复审 | 60 | 90 |
| Test 测试 | 180 | 180 |
| Reporting 报告 | 240 | 150 |
| 合计 | 1210 | 1335 |
从 PSP 记录可以看出,总的开发实际略高于预估耗时,在动手开发前的计划、生成设计文档、设计复审和代码规范等模块耗时其实会比预估的更久。因为这次涉及到双人协作开发,关于开发的规范和限制考虑得会更多,也一直在审核AI给出的开发文档是否合理,会不会产生冲突,任务安排合不合理。先把开发文档写好后面才会更加高效。
同时在具体编码和设计会比预估时间更高,因为在开发过程中出现了页面实现的效果跟之前那次作业的原型出入较大的情况,额外花费了一些时间去调整页面的设计。
四、解题思路与设计实现说明
1. 总体设计思路
项目采用前端单机 Web 架构。
我们没有让每个页面分别保存一份自己的物品信息,而是设计了一套统一的数据服务。
整体结构如下:
用户界面
↓
页面逻辑
↓
业务服务层
↓
数据存储层
↓
localStorage
这样的设计使首页、搜索、详情和“我的发布”等页面都从同一份数据中读取内容。
例如,当用户在“我的发布”中把一条寻物信息修改为“已找到”后:
我的发布
↓
closeItem()
↓
localStorage
↓
首页 / 搜索 / 详情重新查询
↓
全部显示“已找到”
避免了不同页面之间状态不一致的问题。
2. 项目核心业务流程
流程图:

数据流架构图:

3. 数据结构设计
为了方便不同页面之间共享数据,每条失物招领记录使用统一的数据结构:
{
id: "item-001",
ownerId: "demo-user-001",
type: "lost",
name: "黑色蓝牙耳机",
category: "电子设备",
eventDate: "2026-09-26",
location: "东三教学楼",
description: "充电盒外侧有轻微划痕",
image: "assets/images/...",
contactType: "QQ",
contactValue: "123456789",
status: "active",
createdAt: "...",
updatedAt: "..."
}
其中:
type = lost → 寻物
type = found → 招领
状态只在数据层保存:
active
closed
页面显示时再转换为:
lost + active = 寻找中
lost + closed = 已找到
found + active = 待认领
found + closed = 已归还
这样比直接在数据中保存“寻找中”“待认领”“已找到”“已归还”更加统一,也减少了不同页面重复处理状态的情况。
4. 数据存储
项目采用浏览器 localStorage 保存数据。
这样做的好处是:
- 不需要部署数据库;
- 不需要启动后端;
- Chrome 直接打开 HTML 即可运行;
- 发布数据后刷新网页仍然存在。
数据访问统一经过存储服务,而不是让页面直接操作 localStorage。
例如:
页面
↓
item-service.js
↓
store.js
↓
localStorage
项目第一次运行时,如果浏览器中不存在项目数据,则写入内置演示数据。
之后所有新增、搜索、详情查询和状态修改均针对同一份数据进行。
5. 首页与列表查询
首页需要显示当前失物招领信息,并按照发布时间从新到旧排列。
业务层通过统一的 list() 接口读取数据。
这样首页不需要知道数据实际保存在什么位置,只需要:
调用 list()
↓
获得数据
↓
渲染卡片
这种方式降低了页面代码与数据存储之间的耦合。
6. 发布信息
寻物和招领信息均通过统一的新增接口保存。
新增记录时程序会:
检查参数
↓
检查信息类型
↓
清理输入内容
↓
检查必填字段
↓
生成唯一 ID
↓
设置 ownerId
↓
设置 active 状态
↓
写入 localStorage
↓
返回发布成功
为了避免出现“页面提示成功,但实际上没有保存”的情况,程序只有在存储操作真正成功之后才返回成功结果。
7. 搜索与组合筛选
搜索是我负责的主要功能之一。
项目不仅支持关键词搜索,还支持:
关键词 + 寻物/招领 + 物品类别
三个条件组合使用。
核心筛选思路类似:
const items = result.data.filter(function (item) {
const typeMatches =
!type ||
type === "all" ||
item.type === type;
const categoryMatches =
!category ||
category === "all" ||
item.category === category;
const keywordMatches =
!keyword ||
[
item.name,
item.category,
item.location,
item.description
].some(function (value) {
return String(value || "")
.toLowerCase()
.includes(keyword);
});
return (
typeMatches &&
categoryMatches &&
keywordMatches
);
});
关键词不仅可以匹配物品名称,还可以匹配:
类别
地点
描述
例如用户搜索:
东三
即使“东三”不在物品名称中,只要它出现在地点中,也可以找到对应信息。
8. 详情查询
为了避免不同详情页显示同一条静态数据,我们增加:
getById(id)
根据 ID 查询具体记录。
核心逻辑如下:
function getById(id) {
const itemId = cleanString(id);
if (!itemId) {
return createError(
"INVALID_ID",
"物品 ID 不能为空"
);
}
const result = store.load();
if (!result.ok) {
return result;
}
const item = result.data.find(
function (record) {
return record.id === itemId;
}
);
if (!item) {
return createError(
"ITEM_NOT_FOUND",
"没有找到这条信息"
);
}
return {
ok: true,
data: item
};
}
如果访问不存在的 ID,程序不会直接报错或显示空白页面,而是返回明确的错误结果,由页面显示友好提示。
9.“我的发布”
“我的发布”只展示当前演示用户发布的记录。
因此实现:
getByOwner(ownerId)
根据:
ownerId
筛选记录。
获取结果后,再按照发布时间从新到旧排序。
未完成记录可以进入“修改状态”,已经完成的记录只允许查看详情。
10. 状态修改
状态修改是本项目业务逻辑中判断分支较多的功能之一。
核心接口为:
closeItem(id, actorId)
主要逻辑为:
记录是否存在?
├─ 否 → 拒绝
└─ 是
↓
当前用户是不是发布者?
├─ 否 → 拒绝
└─ 是
↓
记录是不是已经完成?
├─ 是 → 拒绝重复修改
└─ 否
↓
status = closed
↓
更新 updatedAt
↓
保存
↓
返回成功
关键代码:
function closeItem(id, actorId) {
const itemId = cleanString(id);
const userId = cleanString(actorId);
if (!itemId) {
return createError(
"INVALID_ID",
"物品 ID 不能为空"
);
}
if (!userId) {
return createError(
"INVALID_ACTOR_ID",
"操作用户 ID 不能为空"
);
}
const result = store.load();
if (!result.ok) {
return result;
}
const index = result.data.findIndex(
function (item) {
return item.id === itemId;
}
);
if (index === -1) {
return createError(
"ITEM_NOT_FOUND",
"没有找到这条信息"
);
}
const oldItem = result.data[index];
if (oldItem.ownerId !== userId) {
return createError(
"FORBIDDEN",
"无权修改这条信息"
);
}
if (oldItem.status === "closed") {
return createError(
"ITEM_ALREADY_CLOSED",
"该信息已经完成,无需重复修改"
);
}
const updatedItem = {
...oldItem,
status: "closed",
updatedAt: new Date().toISOString()
};
const nextItems = result.data.slice();
nextItems[index] = updatedItem;
const saveResult = store.save(nextItems);
if (!saveResult.ok) {
return saveResult;
}
return {
ok: true,
data: updatedItem
};
}
这里没有直接修改旧对象,而是先生成新的数据,只有保存成功后才返回成功。
因此,如果存储失败,原数据不会被错误修改。
五、附加特点设计与展示
根据作业要求,我们除了完成基本功能外,还实现了一些方便用户使用的附加特点。
1. 类别组合筛选
设计意义
当失物招领信息数量较多时,仅使用关键词搜索可能会得到很多结果。
因此我们增加物品类别筛选,例如:
校园卡 / 证件
电子设备
雨具
书籍资料
生活用品
其他
用户可以同时使用:
关键词 + 类型 + 类别
缩小查找范围。
例如:
关键词:耳机
类型:寻物
类别:电子设备
只有同时满足三个条件的记录才会显示。
实现思路
类别条件和关键词、类型共同参与过滤:
return (
typeMatches &&
categoryMatches &&
keywordMatches
);
实现效果

2. 一键复制联系方式
设计意义
如果用户找到自己的物品,需要与发布者联系。
如果只能手动选中文字并复制 QQ、微信或手机号,操作相对繁琐。
因此详情页提供:
复制联系方式
按钮。
实现思路
页面优先调用浏览器 Clipboard API:
navigator.clipboard.writeText(...)
复制成功后显示:
已复制
如果浏览器不支持 Clipboard API 或复制失败,则提示用户手动复制联系方式。
实现效果

3. 页面交互反馈
除了两个主要附加特点外,页面还提供了多种操作反馈:
- 搜索无结果提示;
- 表单错误提示;
- 修改状态确认框;
- 修改状态成功提示;
- 不存在记录的友好错误页面;
- 已完成信息不再显示修改状态入口。
这些设计能够避免用户在操作后不知道程序是否成功执行。
六、目录说明与使用说明
1. 项目目录
项目目录按照页面、样式、数据、业务逻辑、路由控制和测试等功能进行划分。
项目根目录/
├─ index.html
├─ README.md
├─ package.json
├─ assets/
│ ├─ css/
│ ├─ images/
│ └─ js/
│ ├─ core/
│ ├─ data/
│ ├─ services/
│ ├─ ui/
│ ├─ pages/
│ ├─ feature-controller.js
│ ├─ publish-controller.js
│ ├─ router.js
│ └─ app.js
├─ tests/
└─ docs/
各目录及主要文件作用如下:
| 文件 / 目录 | 作用 |
|---|---|
index.html |
网页入口及各页面模板 |
assets/css/ |
页面 CSS 样式 |
assets/images/ |
图片、图标等素材 |
assets/js/core/ |
状态映射、公共逻辑、表单校验等 |
assets/js/data/ |
演示数据及 localStorage 存储 |
assets/js/services/ |
发布、查询、状态修改等核心业务逻辑 |
assets/js/ui/ |
通用 UI 组件及交互逻辑 |
assets/js/pages/ |
各页面的渲染和页面内交互 |
assets/js/feature-controller.js |
连接搜索、详情、个人资料、我的发布等页面与业务逻辑 |
assets/js/publish-controller.js |
连接首页、发布页面、发布成功页面与业务逻辑 |
assets/js/router.js |
统一管理 Hash 路由,根据当前地址将页面请求分发给对应控制器 |
assets/js/app.js |
程序初始化入口,完成数据初始化并启动统一路由 |
tests/ |
Vitest 单元测试 |
package.json |
npm 依赖及测试配置 |
docs/ |
项目相关说明、截图等文档资料 |
项目在结构上将数据、业务逻辑、页面渲染和路由控制进行分离。
其中:
router.js统一监听页面 Hash 地址变化,并将不同页面请求交给对应控制器处理。feature-controller.js负责连接搜索、详情、个人资料、我的发布等页面与业务逻辑。publish-controller.js负责连接首页、发布页面和发布成功页面与业务逻辑。app.js作为整个程序的启动入口,负责完成数据初始化并启动统一路由。
这样的划分能够减少不同模块之间的耦合,也方便后续维护和功能扩展。
2. 网页运行方法
测试人员下载完整项目后:
- 解压项目;
- 使用 Google Chrome;
- 找到项目根目录;
- 双击或使用 Chrome 打开
index.html; - 即可使用网页。
网页运行时:
不需要启动服务器
不需要安装数据库
不需要运行 npm
项目数据保存在当前浏览器的 localStorage 中。
3. 单元测试运行方法
如果需要运行单元测试,需要先安装 Node.js。
进入项目根目录后执行:
npm install
安装依赖。
之后运行:
npm test
即可一次性运行全部自动化测试。
七、单元测试
1. 为什么选择 Vitest
本项目使用 Vitest 进行 JavaScript 单元测试。
在学习单元测试时,我首先理解了“单元测试”和“测试用例”的区别:
- 单元测试:单独验证程序中的一个较小功能,例如一个函数或一个业务接口;
- 测试用例:在某一种具体输入和条件下,检查程序是否能够得到预期结果。
随后,我学习了 Vitest 中最基本的:
describe()
it()
expect()
等使用方式。
选择 Vitest 的原因主要有:
- 对 JavaScript 支持较好;
- 配置和使用比较简单;
- 测试结果清晰;
- 可以自动运行全部测试;
- 适合测试本项目的数据层和业务服务层;
- 可以通过一条
npm test命令快速进行回归测试。
对我来说,使用 Vitest 之后最大的变化是,测试不再只是“打开网页点几下看看有没有报错”,而是可以直接针对某一个函数构造不同输入,检查它在正常、异常和边界情况下是否都符合预期。
2. 一个简单的 Vitest 教程
首先在项目目录中安装依赖:
npm install
一个最简单的 Vitest 测试代码如下:
import {
describe,
it,
expect
} from "vitest";
describe(
"示例测试",
function () {
it(
"1 + 1 应该等于 2",
function () {
expect(
1 + 1
).toBe(2);
}
);
}
);
其中:
describe()
用于将一组相关的测试组织在一起。
it()
表示一个具体的测试用例。
expect()
用于判断程序的实际执行结果是否和预期结果一致。
完成测试代码后,在项目根目录运行:
npm test
Vitest 会自动寻找测试文件并执行其中的测试用例。
3. 当前单元测试情况
本项目目前共有 6 个测试文件:
| 测试文件 | 测试用例数量 |
|---|---|
store.test.js |
3 |
validators.test.js |
6 |
item-service.test.js |
12 |
search.test.js |
7 |
my-posts.test.js |
4 |
status.test.js |
6 |
| 合计 | 38 |
最终执行结果:
Test Files 6 passed (6)
Tests 38 passed (38)
目前 38 个自动化测试均全部通过。
项目共实现 38 个自动化测试用例,其中选取 closeItem(id, actorId) 状态修改逻辑进行白盒测试,根据函数内部的记录存在性、发布者权限、当前状态和存储结果等判断分支构造了 6 个测试用例。
以下为测试结果截图:

4. 测试用例和测试数据的构造
虽然项目目前共有 38 个自动化测试用例,但如果将全部测试代码都放在博客中会比较冗长,因此这里选取一些具有代表性的测试情况进行说明。
在设计测试数据时,我没有只考虑正常输入,而是同时加入了:
正常情况
反例
边界情况
组合条件
异常情况
部分代表性测试用例如下:
| 编号 | 被测功能 | 测试场景 / 测试数据 | 预期结果 | 用例类型 |
|---|---|---|---|---|
| 1 | 数据初始化 | localStorage 中不存在项目数据 | 自动初始化演示数据 | 正常 |
| 2 | 数据初始化 | localStorage 中已经存在数据 | 不覆盖原来的数据 | 边界 |
| 3 | 信息发布 | 填写完整的寻物信息 | 发布成功并保存数据 | 正常 |
| 4 | 信息发布 | 缺少物品名称等必填字段 | 拒绝发布并返回错误 | 反例 |
| 5 | 信息发布 | 模拟数据保存失败 | 返回失败,不能提示发布成功 | 异常 |
| 6 | 详情查询 | 使用存在的物品 ID | 返回对应物品信息 | 正常 |
| 7 | 详情查询 | 使用不存在的物品 ID | 返回 ITEM_NOT_FOUND |
反例 |
| 8 | 搜索 | 关键词匹配物品名称 | 返回对应记录 | 正常 |
| 9 | 搜索 | 关键词匹配地点或描述 | 返回对应记录 | 正常 |
| 10 | 搜索 | 搜索完全不存在的关键词 | 返回空数组 | 边界 |
| 11 | 类型筛选 | 类型选择 lost |
只返回寻物信息 | 正常 |
| 12 | 组合筛选 | 关键词 + 类型 + 类别 | 只返回同时满足条件的数据 | 组合 |
| 13 | 我的发布 | 当前用户存在多条发布信息 | 只返回当前用户的数据 | 正常 |
| 14 | 我的发布 | 当前用户没有发布信息 | 返回空数组 | 边界 |
| 15 | 状态修改 | 发布者关闭寻物记录 | 状态变为 closed,显示“已找到” |
正常 |
| 16 | 状态修改 | 非发布者尝试修改 | 返回 FORBIDDEN |
反例 |
| 17 | 状态修改 | 已完成记录再次修改 | 返回 ITEM_ALREADY_CLOSED |
边界 |
| 18 | 状态修改 | 修改过程中保存失败 | 返回存储错误,旧数据保持不变 | 异常 |
这些测试主要目的是验证程序不仅能够完成正常业务流程,在面对不合理输入或者异常情况时,也能够做出正确处理。
5. 白盒测试设计
根据作业要求,至少需要采用一种白盒测试方法。
本项目选择:
closeItem(id, actorId)
作为主要白盒测试对象。
之所以选择这个函数,是因为状态修改不是简单地把:
active
修改成:
closed
而是需要依次经过多个判断。
其主要逻辑可以概括为:
传入 ID 和当前用户
↓
记录是否存在?
↓
当前用户是不是发布者?
↓
记录是不是已经完成?
↓
修改状态
↓
保存是否成功?
↓
返回最终结果
因此可以根据函数内部不同判断分支设计测试用例。
以下为白盒测试流程图:

针对该函数设计了以下 6 种主要情况:
| 编号 | 测试情况 | 预期结果 |
|---|---|---|
| 1 | 发布者正常关闭寻物记录 | 成功,显示“已找到” |
| 2 | 发布者正常关闭招领记录 | 成功,显示“已归还” |
| 3 | 非发布者修改 | 返回 FORBIDDEN |
| 4 | 记录不存在 | 返回 ITEM_NOT_FOUND |
| 5 | 已完成记录重复修改 | 返回 ITEM_ALREADY_CLOSED |
| 6 | 数据保存失败 | 返回存储错误,旧数据保持不变 |
例如,针对“非发布者修改”这一分支:
const result =
service.closeItem(
"item-001",
"other-user"
);
expect(result.ok).toBe(false);
expect(
result.error.code
).toBe("FORBIDDEN");
这个测试不仅检查程序有没有返回错误,还需要进一步确认:
原来的数据不能被修改
并且不能执行错误的保存操作
通过这种方式,可以根据函数内部的判断结构逐个覆盖关键分支,而不是只验证一次正常修改是否成功。
6. 搜索功能测试数据的构造
搜索功能是本项目中测试数据组合比较多的部分。
为了验证搜索逻辑,测试中分别设计了:
不设置任何条件
关键词匹配名称
关键词匹配地点
关键词匹配描述
只使用类型筛选
只使用类别筛选
关键词 + 类型 + 类别组合
完全没有匹配结果
例如,有以下几条模拟数据:
黑色蓝牙耳机
类别:电子产品
类型:lost
地点:图书馆
校园卡
类别:证件
类型:found
地点:教学楼
如果搜索:
耳机
应该只返回第一条数据。
如果筛选:
类型:found
类别:证件
则应该只返回校园卡。
如果测试人员输入:
qwerty123不存在物品
程序应该正常返回:
[]
而不能出现 JavaScript 报错。
对于组合筛选,还需要保证多个条件之间是:
AND
关系。
也就是说:
关键词满足
并且
类型满足
并且
类别满足
才能进入最终结果。
这样可以避免出现“明明类别不符合,却因为关键词符合仍然被搜索出来”的情况。
7. 如何应对测试人员的“刁难”
除了正常流程外,我在设计测试时也考虑了一些测试人员可能故意进行的异常操作,例如:
- 输入空 ID 或不存在的 ID;
- 尝试修改其他用户发布的信息;
- 对已经完成的记录再次修改;
- 输入完全不存在的搜索关键词;
- 同时使用关键词、类型和类别等多个筛选条件;
- 模拟 localStorage 读写失败。
针对这些情况,程序会分别返回对应错误信息或空结果,并保证原有数据不会被错误修改。
例如,非发布者修改信息时会返回:
FORBIDDEN
记录不存在时会返回:
ITEM_NOT_FOUND
已经完成的记录再次修改时会返回:
ITEM_ALREADY_CLOSED
通过这些测试,可以验证程序不仅在正常情况下能够运行,在异常输入、越权操作、重复操作和存储失败等情况下也能够做出合理处理。
8. 测试结果与测试评价
本项目目前共实现 38 个自动化测试用例,已经超过作业要求中的最低测试数量。
测试范围主要覆盖:
数据存储
表单校验
信息发布
列表查询
详情查询
搜索与筛选
我的发布
状态修改
权限判断
异常处理
其中,状态修改功能使用白盒测试方法,根据 closeItem(id, actorId) 内部的判断结构,对正常修改、越权修改、记录不存在、重复修改和存储失败等分支进行了测试。
测试数据也不仅包含正常数据,还加入了:
空值
错误参数
不存在的数据
边界数据
组合条件
重复操作
越权操作
存储失败
等情况。
以下为具体测试结果:

除了 Vitest 自动化单元测试之外,我们还在 Google Chrome 中实际进行了完整业务流程测试:
发布信息
↓
首页浏览
↓
搜索和筛选
↓
查看详情
↓
联系发布者
↓
我的发布
↓
修改状态
↓
首页 / 搜索 / 详情同步更新
测试过程中重点检查了:
- 发布后的数据是否能够立即被查询;
- 搜索和筛选结果是否正确;
- 详情页是否能够读取对应记录;
- 联系方式是否能够正常显示;
- “我的发布”是否只显示当前用户的数据;
- 状态修改之后多个页面是否保持一致;
- 已完成记录是否能够正确显示“已找到”或“已归还”;
- 异常输入是否能够得到合理反馈。
因此,目前的自动化测试和人工功能测试已经能够较好覆盖本项目的核心功能以及主要异常情况。
不过,本项目目前仍然属于一个基于浏览器 localStorage 的前端单机演示项目,因此测试也存在一定局限,例如暂时没有进行:
- 大规模数据压力测试;
- 多浏览器兼容性测试;
- 多用户并发测试;
- 网络异常测试;
- 服务端安全测试;
- 真正的账号权限和身份认证测试。
如果以后继续将项目扩展为真正的校园失物招领系统,在加入后端、数据库和账号系统之后,还需要进一步增加接口测试、数据库测试、权限测试、网络异常测试以及多用户并发测试。
八、GitHub 协作与代码签入
项目开发过程中,我们没有两个人直接同时修改 main,而是采用:
最新 main
↓
创建 feature 分支
↓
开发
↓
Commit
↓
Push
↓
Pull Request
↓
对方检查
↓
Merge
的方式协作。
例如使用过:
feature/a-store
feature/a-home-data
feature/a-create
feature/a-search
feature/a-detail
feature/a-my-posts
feature/a-status
等功能分支。
Commit 信息尽量明确表达本次修改内容,例如:
feat(search): 支持关键词和类型筛选
feat(status): 实现状态修改与跨页面数据同步
避免使用:
修改
111
最终版
等无法体现实际内容的 Commit 信息。
我们一部分commit历史的截图:

我们一部分PR记录的截图:

通过 PR 的方式开发,也让双方都能检查对方的修改,而不是在项目最后才一次性合并代码。
九、开发中遇到的问题及解决方法
问题一:本地分支没有跟main分支同步
问题描述
开发过程中发现自己的分支没办法跟main分支保持同步,一直点fetch却一直更新不了
做过的尝试
切换了到了其他分支尝试,甚至建了一个新分支
最终解决方法
最后在GitHub desktop找到了Updata from main这个按钮

是否解决
解决
收获
要及时更新本地分支,不然在并行开发的过程中很容易出现冲突和协作问题。
问题二:状态修改成功页返回无效,底部内容被遮挡
问题描述
在实现 F08 状态修改和 F09 页面反馈时,状态修改成功页出现了两个界面问题:左上角的返回按钮点击后没有反应;在较小的浏览器窗口中,状态卡片下半部分会被固定在底部的导航栏挡住,页面又不能继续向下滚动,因此用户看不到完整的“原状态”和“新状态”。
最终解决方法
我将左上角返回按钮和下方返回按钮统一绑定到 onReturn 回调,使两个入口都能回到“我的发布”页面,并在返回后显示状态修改成功的提示。
随后为成功页内容区增加 overflow-y: auto 和底部留白,让底部导航栏继续固定显示,而上方内容可以独立滚动。为了保持原型中的视觉效果,同时隐藏了滚动条,但不影响鼠标滚轮和触控板滚动。
是否解决
已解决。
收获
页面上的控件不能只做出视觉效果,还要检查是否真正接入了交互逻辑。固定导航栏会占用可视空间,因此移动端页面还需要在不同窗口高度下检查内容是否可滚动、底部是否留有足够空间。
十、最终功能完成情况
目前项目已经实现主要功能:
| 编号 | 功能 | 当前情况 |
|---|---|---|
| F01 | 首页浏览 | 已完成 |
| F02 | 发布寻物 | 已完成 |
| F03 | 发布招领 | 已完成 |
| F04 | 搜索与筛选 | 已完成 |
| F05 | 查看详情 | 已完成 |
| F06 | 联系发布者 | 已完成 |
| F07 | 我的发布 | 已完成 |
| F08 | 修改状态 | 已完成 |
| F09 | 页面反馈 | 已完成 |
| F10 | 数据同步 | 已完成 |
自动化测试结果:
Test Files 6 passed
Tests 38 passed
Chrome 中已经能够完成核心业务闭环:
发布信息 → 浏览/搜索 → 查看详情 → 联系发布者 → 查看我的发布 → 修改信息状态
十一、队友评价
1. 值得我学习的地方
本次结对开发中,我的队友 主要负责数据层、业务逻辑和单元测试。
我认为队友比较值得我学习的地方是:
首先,队友对数据结构和业务接口的设计比较清晰。规范了数据结构:
举例:
{
type: "lost",
name: "黑色蓝牙耳机",
category: "电子设备",
eventDate: "2026-10-03",
location: "东三教学楼",
description: "充电盒外侧有轻微划痕",
image: "",
contactType: "QQ",
contactValue: "123456789"
}
,并提供了 create、getById、getByOwner、closeItem 等接口,使我负责的页面可以直接调用。
其次,队友编写单元测试时考虑得比较全面。除了正常的新增、查询和状态修改,还测试了记录不存在、非发布者修改、重复修改以及保存失败等异常情况。
另外,队友在联调阶段能够及时补充接口并处理路由问题。将多个控制器整理为统一的 router.js。
2. 可以改进的地方
我和队友在接口约定和改动说明方面还可以进一步改进。
由于数据接口和页面是并行开发的,他负责数据接口我来实现页面,如果能尽早做一个飞书文档来共享两个人的进度开发效果会更好,同时在文档中说明接口的改动和实现,防止出现合并错误和接口出错。
十二、个人总结
1.协作开发及时沟通很重要
在开发过程中及时同步进度,能有效防止后续返工和合并冲突。对可能影响整体效果的变动也要及时在双方规定的开发文档中进行修改。本次作业只是双人开发,如果是多人开发,及时更新进度和模块变动更为重要。
2.工具使用很重要
不同于传统的git命令行方式,GitHub desktop让pr、clone、commit和、Crete branch、push和pull等操作更加简单和可视化,只需要一个按钮就可以完成,非常方便。
3.开发前规范开发文档很重要
在团队开发中,提前安排好开发人员的任务和代码规范很关键。要做到一个人的任务不易和其他人冲突,既一个人负责一个或若干个类似模块或者功能,如果进度不一致需要先开发的人留好接口和说明文档。
4.队友靠谱很重要
这把罗神依旧靠谱,既完成了测试的模块也找出来好几个bug,还完成了博文主体部分的撰写。
浙公网安备 33010602011771号