软件工程结对作业(第二次之程序实现)
软件工程结对作业(第二次之程序实现)
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 202601 软件工程 |
| 这个作业要求在哪里 | 2026秋软件工程结对作业(第二次之程序实现) |
| 这个作业的目标 | 将校园失物招领原型实现为可运行的 Web 应用,完成核心业务流程,并通过 GitHub 协作、PSP 和单元测试实践结对开发、版本管理与软件测试。 |
| 设计原型稿与在线演示 | Figma 设计稿 , Figma 在线演示 |
| GitHub 仓库 | 102401310-102401312 |
| 结对成员 | 102401310 庄凯堃、102401312 刘谦益 |
| 本篇博客 | 庄凯堃博客 |
| 队友博客 | 刘谦益博客 |
一、项目简介
1. 项目背景
校园里的失物招领信息常分散在不同群聊中,容易被新消息覆盖,查找和跟进都不方便。我们基于第一次结对作业的原型设计,实现了“拾光校园”失物招领网页,将寻物和招领信息集中展示,方便同学发布信息、查找线索和联系发布者。
2. 核心功能
项目围绕以下流程展开:
发布信息 → 浏览或搜索 → 查看详情 → 联系发布者 → 更新状态
- 发布信息:选择寻物或招领,填写物品特征、时间、地点及联系方式。
- 浏览或搜索:浏览最新信息,或通过关键词查找相关记录。
- 查看详情:核对物品描述、照片、时间、地点和当前状态。
- 联系发布者:查看并复制联系方式,通过对应渠道在站外沟通。
- 更新状态:物品找回或归还后,由发布者在“我的”中标记“已找到”或“已归还”,减少无效联系。
在基本流程之外,还增加了组合筛选、自定义日期范围、相似信息提醒、图片上传、草稿保存和搜索历史等便利功能。
二、具体分工与合作情况
1. 具体分工
两人按功能模块分工,共同完成需求讨论、接口约定和功能联调。
| 成员 | 主要负责内容 |
|---|---|
| 庄凯堃 | 搜索与详情:关键词搜索、分类与区域筛选、时间范围筛选、排序、搜索历史、详情展示及联系方式复制。 扩展功能:图片上传与展示、草稿保存与恢复、相似信息提醒、联系方式格式校验及本人发布删除。 测试与文档:编写和运行自动化测试,整理 README 及个人博客材料。 |
| 刘谦益 | 发布与管理:基础寻物与招领发布、“我的发布”、记录编辑、完成标记与撤销、状态筛选与统计。 页面联动:首页和搜索结果隐藏已完成信息。 视觉调整:首页、搜索页和发布页的布局优化,统一页面版式及图片资源。 |
2. 协作过程
2.1 共同工作
| 环节 | 共同完成的内容 |
|---|---|
| 需求分析 | 根据原型确定核心功能,梳理“发布—浏览或搜索—详情—联系—更新状态”的使用流程,划分开发任务。 |
| 接口约定 | 统一物品字段、信息类型、状态及本地存储方式,明确各模块的数据读取和更新规则。 |
| 功能联调 | 检查发布、编辑、完成、恢复和删除操作能否同步更新首页、搜索结果及“我的发布”。 |
| 交叉检查 | 检查对方负责的模块,结合正常操作和异常输入排查问题,调整交互及页面显示。 |
| 材料整理 | 汇总实现思路、开发问题、测试结果和演示材料,完成作业博客。 |
2.2 GitHub 协作方式
- 分别开发:庄凯堃维护主仓库,刘谦益 fork 仓库,在各自电脑上完成负责的模块。
- 提交与合并:功能完成后提交代码,刘谦益通过 Pull Request 提交修改,检查双方改动并处理冲突后合入主仓库。
- 同步与联调:将主仓库的最新代码拉取到本地,再检查模块之间的衔接和完整流程。
3. PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 60 | 40 |
| Estimate | 估计这个任务需要多少时间 | 60 | 40 |
| Development | 开发 | 1050 | 1520 |
| Analysis | 需求分析(包括学习新技术) | 120 | 170 |
| Design Spec | 生成设计文档 | 60 | 60 |
| Design Review | 设计复审 | 60 | 90 |
| Coding Standard | 代码规范(为目前的开发制定合适的规范) | 30 | 30 |
| Design | 具体设计 | 60 | 190 |
| Coding | 具体编码 | 480 | 600 |
| Code Review | 代码复审 | 60 | 80 |
| Test | 测试(自我测试、修改代码、提交修改) | 180 | 300 |
| Reporting | 报告 | 150 | 240 |
| Test Report | 测试报告 | 60 | 120 |
| Size Measurement | 计算工作量 | 30 | 30 |
| Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 60 | 90 |
| 合计 | 1260 | 1800 |
预估总耗时为 1260 分钟,实际为 1800 分钟,超出 540 分钟,约 42.9%。其中,具体设计增加 130 分钟,编码和测试各增加 120 分钟,三项合计占总增量的 68.5%,是主要超时来源,与页面反复调整及附加功能扩展有关。
后续应先明确核心功能和页面规范,按优先级安排附加功能;新增功能时同步调整时间预算,并及时记录耗时,为测试、文档整理和返工预留时间。
三、解题思路描述与设计实现说明
1. 代码实现思路
1.1 按核心流程划分模块
我们先根据“发布信息 → 浏览或搜索 → 查看详情 → 联系发布者 → 更新状态”的流程划分功能,再确定各模块之间的数据传递方式。
| 模块 | 主要职责 | 对应文件 |
|---|---|---|
| 页面切换 | 切换首页、搜索、详情、发布等视图,更新导航状态 | js/app.js |
| 数据管理 | 统一读取、保存、修改和删除记录 | js/data.js |
| 首页与卡片 | 展示最新信息,生成可复用的物品卡片 | js/home.js、js/card.js |
| 搜索 | 关键词匹配、条件筛选及结果排序 | js/search.js |
| 发布 | 表单校验,完成新建与编辑操作 | js/publish.js |
| 详情 | 展示物品信息、照片及联系方式 | js/detail.js |
| 本人发布管理 | 展示本人记录,更新状态、编辑或删除信息 | js/mine.js |
项目采用原生 HTML、CSS 和 JavaScript,分别负责页面结构、样式和交互。各视图集中在 index.html 中,通过 showView() 控制显示与隐藏;公共卡片和工具函数统一复用,减少重复代码。
1.2 统一数据结构与存储入口
寻物和招领使用同一种记录结构,通过 type 区分类型,避免分别维护两套数据。主要字段如下:
| 字段 | 含义 |
|---|---|
id、ownerId |
信息编号、当前浏览器中的发布者标记 |
type |
lost 表示寻物,found 表示招领 |
name、category、description |
物品名称、分类及外观特征 |
region、location、eventTime |
所在区域、具体地点及丢失或拾取时间 |
contactMethod、contact |
联系渠道及联系方式 |
images |
物品照片 |
status、createdAt |
当前状态及发布时间 |
各模块通过统一数据层 ShiguangData 操作记录,由数据层写入 localStorage,并维护页面使用的内存列表。
这种方式便于首页、搜索页和“我的发布”使用同一份数据。数据刷新后仍可恢复,但仅保存在当前浏览器中,不会跨电脑或浏览器同步。
1.3 串联核心业务流程
在模块和数据结构确定后,再按照用户操作顺序实现完整流程。
| 步骤 | 实现方式 |
|---|---|
| 发布信息 | 用户选择寻物或招领并填写表单。提交前检查必填项、时间、字段长度及联系方式格式;校验通过后生成编号和发布时间,将记录保存为进行中状态。保存失败时提示错误并保留输入。 |
| 浏览或搜索 | 首页展示最新三条未完成信息。搜索页先匹配关键词,再结合类型、分类、区域和时间筛选,最后排序并生成结果卡片。 |
| 查看详情 | 点击卡片后读取对应记录,展示名称、特征、照片、时间、地点和当前状态,同时累加浏览次数。 |
| 联系发布者 | 详情页展示联系渠道和联系方式,提供复制入口。用户通过对应渠道在站外沟通,系统不实现即时聊天。 |
| 更新状态 | 原发布者进入“我的”,将寻物标记为“已找到”、招领标记为“已归还”。修改前核对记录归属,保存成功后同步更新相关页面。 |
状态使用 active 和 resolved 两个值,显示文字根据类型区分:
| 信息类型 | 进行中(active) | 已完成(resolved) |
|---|---|---|
| 寻物 | 寻找中 | 已找到 |
| 招领 | 等待认领 | 已归还 |
已完成记录从首页和搜索结果中隐藏,继续保留在“我的发布”中;撤销完成标记后,重新参与展示。
2. 关键实现的流程图

图中重点展示发布、编辑和状态修改如何影响其他页面。用户操作交给数据层处理后,程序按照以下顺序执行:
校验输入或记录归属 → 写入本地存储 → 更新内存数据 → 通知相关页面重新渲染。
这里有两个关键处理:
- 保存成功后才更新页面数据:如果写入失败,后续内存更新和成功通知不会执行,调用页面显示错误,避免界面显示成功但刷新后修改丢失。
- 通过统一事件刷新列表:数据变化后派发
shiguang:datachange,首页、搜索页、“我的发布”和相似信息面板监听事件并重新渲染,减少各模块之间的直接调用。
3. 重要代码片段
代码片段一:统一保存修改,保持数据与页面一致
代码来源:js/data.js:
function updateOwnedRecord(id, changes) {
const { saved, record } = readOwnedRecord(id);
const updated = { ...record, ...changes };
localStorage.setItem(
STORAGE_KEY,
JSON.stringify(saved.map(item =>
item === record ? updated : item
))
);
const index = items.findIndex(
item => item.id === id &&
item.ownerId === record.ownerId
);
if (index !== -1) items[index] = updated;
notifyItemsChanged();
return updated;
}
function notifyItemsChanged() {
document.dispatchEvent(
new CustomEvent("shiguang:datachange")
);
}
这段代码是编辑信息和更新状态共用的保存入口,处理顺序如下:
- 读取并核对记录:
readOwnedRecord()从存储中读取最新记录,并检查它是否属于当前浏览器中的发布者。 - 合并修改内容:通过展开运算符合并原记录与修改字段,保留其他信息。
- 先保存,再更新:
localStorage.setItem()成功后才替换内存记录。若写入抛出异常,后续操作停止,由调用页面处理错误。 - 通知页面刷新:派发统一事件,使相关列表重新读取数据并渲染。
将这些步骤集中处理,可以避免编辑和状态修改各自维护一套保存逻辑,也能减少存储、内存和页面显示不一致的问题。
代码片段二:根据关键词相关性与发布时间排序
代码来源:js/search.js:
function keywordScore(item, searchKeyword) {
const query = String(searchKeyword || "")
.trim().toLocaleLowerCase();
if (!query) return 0;
const name = String(item.name || "")
.toLocaleLowerCase();
if (name === query) return 4;
if (name.includes(query)) return 3;
if (String(item.category || "")
.toLocaleLowerCase().includes(query)) {
return 2;
}
return [item.description, item.location].some(
value => String(value || "")
.toLocaleLowerCase().includes(query)
) ? 1 : 0;
}
function sortSearchItems(
source, order = "default", searchKeyword = ""
) {
const timestamp = item =>
new Date(item.createdAt).getTime();
return [...source].sort((a, b) => {
if (order === "default") {
const relevance =
keywordScore(b, searchKeyword) -
keywordScore(a, searchKeyword);
if (relevance) return relevance;
}
if (order === "views") {
const views =
ShiguangData.viewCount(b) -
ShiguangData.viewCount(a);
if (views) return views;
}
const left = timestamp(a);
const right = timestamp(b);
if (!Number.isFinite(left)) {
return Number.isFinite(right) ? 1 : 0;
}
if (!Number.isFinite(right)) return -1;
return right - left;
});
}
搜索先通过 filterItems() 得到符合条件的记录,再交给排序函数处理,避免把筛选与排序混在一起。
- 计算相关性:名称完全匹配得 4 分,名称包含关键词得 3 分,分类匹配得 2 分,描述或地点匹配得 1 分。例如搜索“钥匙”时,名称包含“钥匙”的记录优先于只在描述中提到“钥匙”的记录。
- 处理不同排序方式:默认按相关性排序,浏览次数排序则优先比较浏览量;相关性或浏览次数相同时,再按发布时间从新到旧排列。
- 处理缺失时间:有效发布时间优先于无效时间,避免异常数据影响正常记录的顺序。
- 避免修改原数组:使用
[...source]创建副本后再调用sort(),防止排序影响其他模块使用的数据顺序。
四、附加特色设计与展示
在完成“发布信息—浏览或搜索—查看详情—联系发布者—更新状态”的基本流程后,我们围绕三个使用问题进行扩展:发布前如何发现已有线索、搜索结果较多时如何缩小范围、填写中断后如何减少重复操作。
下面重点介绍相似信息提醒和组合筛选,并汇总其他便利功能。
特点一:发布时的相似信息提醒
1.1 设计意义
用户准备发布信息时,可能还没有查看已有记录。例如,一位同学准备发布捡到校园卡的招领信息,而失主已经发布了寻物启事,双方仍需要再次搜索才能发现对方。
因此,我们将线索匹配融入发布过程:用户填写名称或选择分类时,页面自动展示相关记录,并优先展示与当前发布类型相反的信息。发布招领时优先展示寻物,发布寻物时优先展示招领,方便用户在填写过程中发现联系线索。
相似结果只是候选信息,是否为同一件物品仍需用户核对。
1.2 实现思路
处理流程为:
整理输入 → 筛选候选记录 → 计算匹配分数 → 排序 → 展示结果。
- 整理输入:统一名称大小写,去除空白、标点和符号,减少输入形式差异。
- 筛选候选记录:排除已完成的信息和指定编号的记录。
- 计算匹配分数:根据名称相似程度计分,分类相同则额外加分。
- 排列结果:相反类型优先,其次比较匹配分数和发布时间,最多展示三条。
- 连接详情页:点击候选信息后进入详情,进一步核对特征及联系方式。
匹配规则如下:
| 匹配情况 | 计分规则 |
|---|---|
| 名称完全相同 | 100 分 |
| 名称存在包含关系 | 70 分 |
| 相邻双字符重合度不低于 0.5 | 重合度 × 50,四舍五入 |
| 分类相同 | 额外加 20 分 |
名称不足两个字符且未选择分类时,不展示候选结果;只选择分类时,也可以展示同分类的候选记录。
1.3 关键代码及解释
代码来源:js/similar.js。以下摘取名称相似度计算及排序部分。
(1)计算相邻双字符的重合程度
const pairs = value => new Set(
Array.from(
{ length: Math.max(0, value.length - 1) },
(_, i) => value.slice(i, i + 2)
)
);
const namePairs = pairs(name);
const candidatePairs = pairs(candidate);
const common = [...namePairs]
.filter(pair => candidatePairs.has(pair)).length;
const overlap =
2 * common / (namePairs.size + candidatePairs.size || 1);
if (overlap >= 0.5) {
score = Math.round(overlap * 50);
}
名称不完全相同、也不存在包含关系时,程序比较相邻双字符片段。例如,“黑色折叠伞”可以拆分为“黑色”“色折”“折叠”“叠伞”,再与候选名称的片段计算重合程度。
这种方式允许名称存在部分差异,但不理解物品的真实含义,因此仅用于提供候选线索。
(2)按业务关系和匹配程度排序
.sort((a, b) =>
Number(b.opposite) - Number(a.opposite) ||
b.score - a.score ||
(Date.parse(b.item.createdAt) || 0) -
(Date.parse(a.item.createdAt) || 0)
)
.slice(0, 3)
.map(entry => entry.item);
排序首先比较是否为相反类型,让寻物者优先看到招领信息;类型优先级相同时,再比较分数和发布时间。限制为三条,可以避免候选信息过多干扰表单填写。
1.4 实现成果展示

展示流程:
- 在发布页输入物品名称或选择分类。
- 查看页面提供的候选信息。
- 点击候选记录进入详情,核对物品特征及联系方式。
特点二:搜索时的多条件组合筛选
2.1 设计意义
关键词搜索能够找到相关信息,但“校园卡”“钥匙”等常见物品可能对应多条结果。用户通常还记得地点或时间,例如“最近几天在图书馆丢失的校园卡”,仅靠关键词难以利用这些线索。
因此,我们在关键词搜索基础上增加类型、分类、区域和时间筛选,支持多个条件同时使用。时间还支持自定义日期范围,方便用户查找某段时间内的信息。
用户可以根据已有线索逐步缩小结果范围,减少逐条打开详情的操作。
2.2 实现思路
处理流程为:
整理关键词 → 排除已完成信息 → 组合条件筛选 → 排序 → 更新结果。
- 匹配关键词:去除首尾空白、统一大小写,在名称、分类、描述和地点中匹配。
- 组合筛选条件:记录必须满足全部已选条件;未选择的条件不限制结果。
- 处理时间范围:近 7 天、近 30 天按当前时间计算,自定义范围包含开始和结束日期当天。
- 兼容旧数据:优先使用区域字段,旧记录缺少该字段时再通过地点文字判断,例如将实验楼归入教学区。
- 更新结果:刷新卡片和数量,无匹配记录时展示提示及“发布寻物”入口。
2.3 关键代码及解释
代码来源:js/search.js。以下摘取组合判断和自定义日期处理部分。
(1)所有已选条件必须同时满足
return typeMatches &&
categoryMatches &&
searchableText.includes(query) &&
(
!options.region ||
(
item.region
? item.region === options.region
: matchesRegion(item.location, options.region)
)
) &&
timeMatches;
各项条件使用 && 连接。例如,搜索“校园卡”并选择“招领、图书馆、近 7 天”,结果必须同时满足这些条件。
区域未选择时直接放行;选择后优先比较结构化字段,缺少字段的旧记录再通过地点文字匹配。
(2)完整包含结束日期当天
if (
options.days === "custom" &&
(options.startDate || options.endDate)
) {
const start = options.startDate
? eventTimestamp(options.startDate)
: -Infinity;
const end = options.endDate
? new Date(`${options.endDate}T00:00`)
: null;
if (end) end.setDate(end.getDate() + 1);
const endExclusive = end ? end.getTime() : Infinity;
timeMatches =
Number.isFinite(time) &&
time >= start &&
time < endExclusive;
}
自定义日期采用“包含开始日零点、排除结束日期次日零点”的区间。
例如,选择 10 月 1 日至 10 月 8 日,程序保留不早于 10 月 1 日零点、且早于 10 月 9 日零点的记录,完整包含 10 月 8 日当天。只填写一端时,另一端不设限制;无效事件时间不会通过该范围筛选。
2.4 实现成果展示

展示流程:
- 输入关键词,选择类型、分类和区域。
- 切换近 7 天、近 30 天或自定义日期范围。
- 观察结果数量变化,并通过排序调整展示顺序。
其他便利特点
除以上两项重点特色外,项目还实现了以下扩展:
| 功能 | 实现方式与作用 |
|---|---|
| 草稿自动保存与恢复 | 自动保存未提交的表单内容和图片,刷新后可继续填写;支持主动清除,发布成功后自动清除。编辑已有记录时不覆盖新发布草稿。 |
| 图片上传与放大查看 | 最多上传三张 JPG、PNG 或 WebP 图片,单张原图不超过 10 MB;检查并压缩后保存,详情页支持放大查看,辅助核对物品。 |
| 未上传图片时的分类图案 | 为没有照片的记录提供分类图案,使卡片展示保持一致。分类图案不代表物品实拍图。 |
| 一键复制联系方式 | 点击按钮复制联系方式,失败时尝试备用方式,仍失败则提示手动复制,减少输入操作和错误。 |
| 联系渠道对应校验 | 根据电话、邮箱、QQ、微信等渠道检查格式并显示提示,减少无效输入。格式正确不代表账号真实有效。 |
| 搜索历史下拉列表 | 保存最近十条有效关键词,去重并将最近搜索置顶,支持重新搜索、删除单条和清空记录。 |
| 浏览次数展示与排序 | 每次打开详情累加次数,通过小眼睛图标展示,并支持按次数排序。计数仅保存在当前浏览器中。 |
| 搜索无结果时发布寻物 | 提供“发布寻物”入口,在不覆盖已有填写内容的前提下带入关键词和分类,衔接查找与发布。 |
| 编辑、删除与撤销完成标记 | 支持修改或删除本人信息,并恢复误标记为完成的记录,方便维护发布内容。 |
五、目录说明和使用说明
1. 目录说明
项目按“页面结构、图片资源、样式、功能脚本和开发文档”组织目录,主要文件如下:
102401310-102401312/
├── index.html # 网页入口及各页面结构
├── assets/ # 校园背景、校徽等图片及图标许可
├── css/
│ ├── style.css # 样式入口
│ ├── base.css # 基础样式
│ ├── common.css # 公共组件样式
│ ├── layout.css # 公共布局
│ ├── card.css # 物品卡片
│ ├── home.css # 首页
│ ├── search.css # 搜索页
│ ├── publish.css # 发布页
│ ├── detail.css # 详情页
│ ├── mine.css # “我的”页面
│ ├── reference.css # 页面视觉调整
│ ├── search-reference.css # 搜索页视觉调整
│ └── publish-reference.css # 发布页视觉调整
├── js/
│ ├── app.js # 应用初始化与页面切换
│ ├── data.js # 数据读取、保存与更新
│ ├── utils.js # 时间格式等公共工具
│ ├── card.js # 通用物品卡片
│ ├── home.js # 首页信息展示
│ ├── search.js # 搜索、筛选、排序与搜索记录
│ ├── publish.js # 发布、编辑、图片上传与草稿
│ ├── similar.js # 相似信息提醒
│ ├── detail.js # 详情、图片查看与联系方式复制
│ └── mine.js # 本人发布管理
├── docs/
│ ├── PSP.md # PSP 时间记录
│ └── data-contract.md # 数据字段与接口约定
└── README.md # 项目说明
目录组织遵循以下原则:
- 页面集中组织:各视图写在
index.html中,由app.js控制切换。 - 样式和功能按职责拆分:公共样式与各页面样式分开,JavaScript 按业务模块组织。
- 公共逻辑统一复用:数据由
data.js管理,卡片由card.js生成,时间格式和复制等工具集中在utils.js。 - 资源和文档独立存放:图片及图标许可放在
assets/,开发记录放在docs/,运行说明写入 README。
网页使用原生 HTML、CSS 和 JavaScript,没有运行时第三方框架依赖。本地单元测试另外使用 tests/、package.json 和 vitest.config.mjs,正常使用网页不需要安装测试依赖。
2. 运行方式
推荐使用电脑端 Google Chrome。项目不需要安装依赖、配置数据库或启动后端。
- 在 GitHub 仓库 中点击 Code → Download ZIP,下载并解压项目。
- 进入包含
index.html、assets/、css/和js/的项目根目录。 - 使用 Chrome 打开
index.html,即可使用。
请保留完整目录,不要只复制 index.html。修改代码后,可按 Ctrl + F5 刷新页面。
3. 使用说明
基本流程:发布信息 → 浏览或搜索 → 查看详情 → 联系发布者 → 更新状态。
3.1 发布信息
- 选择类型:进入“发布”页面,丢失物品选择“寻物”,捡到物品选择“招领”。
- 填写信息:填写物品名称、分类、丢失或拾取时间、区域、具体地点、外观特征,以及联系渠道和联系方式。
- 上传照片:可选,最多三张,支持 JPG、PNG、WebP,单张原图不超过 10 MB;未上传时显示默认分类图案。
- 查看相似信息:填写过程中若出现相似信息,可先查看是否与自己的物品有关。
- 提交发布:点击“发布信息”。必填项缺失或联系方式格式不正确时,按照提示修改;发布成功后,可查看详情。
- 管理草稿:未提交的内容会自动保存,再次进入可继续填写;点击“清除草稿”可重新开始,成功发布后草稿自动清除。
发布成功后,点击“查看我的发布”,再点击对应物品卡片进入详情页
3.2 浏览或搜索
-
浏览首页:首页展示最新的三条有效信息,点击“查看全部”进入搜索页。
-
输入关键词:输入物品名称、外观特征或地点,点击搜索按钮或按回车键。
-
筛选结果:
筛选项 可选内容 信息类型 全部、寻物、招领 物品分类 证件卡片、钥匙、电子设备等 所在区域 教学区、图书馆、食堂、宿舍区、操场等,教学区包含实验楼 时间范围 全部时间、近 7 天、近 30 天、自定义起止日期,依据丢失或拾取时间筛选 -
调整排序:选择默认排序、发布时间或浏览次数。
-
使用搜索记录:点击搜索框,在下拉列表中选择历史关键词,也可以删除单条记录或清空记录。
-
处理无结果情况:调整关键词或筛选条件;需要留下寻找信息时,点击“发布寻物”。
3.3 查看详情
- 打开信息:点击物品卡片进入详情页。
- 核对内容:查看物品名称、分类、外观特征、区域、具体地点、丢失或拾取时间、发布时间和当前状态。
- 查看照片:有照片时,点击放大查看。
- 查看浏览次数:卡片上的小眼睛表示浏览次数,每次打开详情都会计数,仅保存在当前浏览器中。
3.4 联系发布者
- 获取联系方式:在详情页查看联系渠道和联系方式。
- 复制并联系:点击复制按钮,通过对应的 QQ、微信、电话、邮箱等渠道联系发布者。
- 确认与交接:进一步核对物品特征,确认后约定交接方式。沟通和交接在网站外完成。
3.5 更新状态
- 找到本人记录:发布者进入“我的”页面,查看自己的发布信息,可按全部、进行中、已完成切换。
- 标记完成:物品找回后,将寻物标记为“已找到”;物品归还后,将招领标记为“已归还”。
- 查看完成记录:已完成的信息保留在“我的”页面,不再出现在首页和搜索结果中。
- 恢复状态:需要继续寻找或招领时,可恢复为进行中。
- 编辑或删除:信息有误时可编辑;不再需要时可删除,删除前需要确认。
项目没有账号登录,“我的发布”通过当前浏览器的身份标记识别,请使用发布时的浏览器和访问地址管理记录
4. 数据保存与重置
- 保存范围:发布信息、图片、草稿、搜索历史和浏览计数保存在当前浏览器站点的 localStorage 中。刷新后可以保留,不同电脑、浏览器或访问地址之间不会自动同步,上传 GitHub 也不会上传这些数据。
- 重置方法:按 F12,进入 Application → Local Storage → 当前项目地址,删除以
shiguang_开头的数据并刷新。该操作会清除当前站点的项目数据,并重新显示默认示例信息。 - 测试提醒:示例日期较早,测试“近 7 天”时可能没有结果,可切换全部时间或发布近期信息;首页仅展示最新三条有效记录,其余记录通过“查看全部”查看。
目录与使用步骤也已整理到仓库的 README,供下载项目的测试人员参照。
六、单元测试
本次测试围绕“发布信息—浏览或搜索—查看详情—联系发布者—更新状态”的核心流程展开,使用 Vitest 和 jsdom 编写了 50 个自动化用例,检查业务函数及部分页面联动。测试脚本在工具辅助下编写,再由本机实际运行并保存结果截图。下面依次介绍工具与学习过程、用例设计、测试代码、运行结果和测试评价。
1. 测试工具与学习过程
1.1 工具选择
项目使用原生HTML、CSS和JavaScript,数据保存在localStorage中。例如“标记已找到”不仅要改变页面文字,还要更新存储,并通知首页和搜索页刷新。因此,我们需要测试真实函数和数据变化,而不能只靠打开网页检查有没有报错。
本次使用的工具及版本如下:
| 工具 | 版本 | 在本项目中的用途 |
|---|---|---|
| Node.js | 22.12.0 | 运行测试环境 |
| npm | 10.9.0 | 安装并管理测试依赖 |
| Vitest | 3.2.7 | 执行用例、断言结果和模拟异常 |
| jsdom | 26.1.0 | 提供document、window和localStorage等对象 |
选择这套工具主要考虑三点:
- 能测试现有代码:不要求把网页改成
Vue或React,可以直接加载原来的JavaScript。 - 能检查失败情况:使用
vi.spyOn()模拟保存失败,检查旧记录是否被破坏。 - 能重复运行:一条
npm test运行全部用例,修改代码后可以再次检查。
1.2 学习与调试过程
学习时,我们先理解单元测试与回归测试的作用,再学习 describe、it 和 expect 的用法,随后配置 Vitest 环境,从项目中的一个实际函数开始编写和运行测试。单元测试用于检查具体函数的行为;回归测试则是在修改代码后重新执行已有用例,检查原有功能是否受到影响。
学习和调试过程分为四步:
- 从一个函数开始:先给
statusText()传入lost、active,检查返回“寻找中”,理解“准备数据—调用函数—比较结果”。 - 解决文件发现问题:第一次
npm test提示“No test files found”。创建tests/utils.test.js后,Vitest才找到测试文件。 - 解决源码读取问题:使用
new URL(..., import.meta.url)读取源码时出现“The URL must be of scheme file”。改用resolve(process.cwd(), "js", "utils.js")定位文件后,第一个用例通过。 - 逐步扩展:补齐四种状态文字,再增加状态更新、搜索日期、存储失败和表单交互;最终运行50个用例。
这两次报错发生在测试环境准备阶段。我们从中也认识到:测试没有运行、测试断言失败和业务逻辑出错,需要分别排查。
1.3 Vitest简易使用教程
1.3.1 安装依赖和配置命令
以下是首次搭建测试环境的步骤。在项目根目录执行;已有 package.json 时跳过第一条:
npm init -y
npm install -D vitest@3 jsdom@26
npm pkg set "scripts.test=vitest run"
npm pkg set "scripts.test:watch=vitest"
创建vitest.config.mjs:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
include: ["tests/**/*.test.js"]
}
});
environment指定模拟环境;include指定只收集tests目录下的.test.js文件。
这些依赖仅用于测试,正常打开网页仍不需要安装。
1.3.2 编写和理解用例
配套项目已有的helpers.js,可以按下面的方式编写用例:
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { loadApp } from "./helpers.js";
let app;
beforeEach(() => { app = loadApp(); });
afterEach(() => { app.dom.window.close(); });
describe("信息状态文字", () => {
it("进行中的寻物显示寻找中", () => {
const item = { type: "lost", status: "active" };
const actual = app.utils.statusText(item);
expect(actual).toBe("寻找中");
});
});
describe将相关用例组织在一起。it描述一次具体检查。expect(actual).toBe(...)比较实际返回值与预期文字。beforeEach准备环境,afterEach在执行后清理。
helpers.js读取真实index.html及JavaScript源码,再在新的JSDOM中执行,向测试提供函数接口。每个用例使用独立页面和存储,既不依赖前一个用例,也不清理真实Chrome中的信息。
项目的数据还保存在内存数组中,因此仅调用localStorage.clear()不够。重新建立环境可以一并隔离内存变量、页面元素和事件监听器。
1.3.3 运行与回归测试
运行全部测试:
npm test
查看每条用例,或在修改后自动重新执行:
npm test -- --reporter=verbose
npm run test:watch
如果已经取得完整的测试文件、配置文件和 package-lock.json,只需在项目根目录执行 npm ci 安装锁定的依赖,再执行 npm test。只下载网页文件而没有测试脚本时,不能直接复现这 50 个用例。
1.3.4 单独运行与自动化方式
单独检查重点模块,并显示每个用例的结果:
npx vitest run tests/data.test.js --reporter=verbose
npx vitest run tests/search.test.js --reporter=verbose
- 批量执行:
npm test自动收集并运行测试文件,不需要手动逐条调用函数。 - 修改后复查:修改数据、搜索或发布逻辑后,再次执行全部测试,检查原有功能。
- 开发时重跑:
npm run test:watch在测试进程运行期间监听相关文件变化并重新测试。
2. 测试数据与用例设计
2.1 测试范围
本次测试在本地进行,测试脚本放在项目根目录的 tests/ 中:
tests/
├── helpers.js # 加载真实代码、准备独立环境与基础数据
├── utils.test.js # 基础函数
├── data.test.js # 数据及状态管理
├── search.test.js # 搜索、日期和排序
└── workflows.test.js # 表单及页面交互
| 测试文件 | 用例数 | 主要检查内容 |
|---|---|---|
| utils.test.js | 8 | 状态文字、时间显示、图片过滤 |
| data.test.js | 13 | 发布、本人身份、状态、保存失败、删除、草稿和浏览次数 |
| search.test.js | 9 | 关键词、组合筛选、区域归并、日期边界和排序 |
| workflows.test.js | 20 | 表单、编辑、详情、页面同步、图片和联系方式复制 |
| 合计 | 50 | 函数级单元测试及部分DOM集成测试 |
2.2 测试数据的构造思路
本次采用白盒分支设计、等价类划分和边界值分析构造数据。先阅读状态更新、搜索和发布校验的代码,确定判断条件;再建立一条有效基础记录,分别构造正常、无效、边界和异常输入,避免一个用例同时引入多个无关错误。
2.2.1 基础记录
基础数据来自helpers.js:
item({
id: "test-001",
ownerId: "owner-a",
type: "lost",
status: "active",
name: "黑色耳机",
category: "电子设备",
description: "银色充电盒",
region: "library",
location: "图书馆一楼",
eventTime: "2026-10-08T10:00",
contactMethod: "QQ",
contact: "12345678"
});
item()提供有效默认字段,测试按目的覆盖。测试号码只是用于格式判断,不代表真实联系方式;发布接口会重新生成ID,因此创建测试比较返回ID和存储ID是否一致。
2.2.2 数据分类
构造数据时分为以下几类:
- 正常数据:有效表单、本人更新、搜索命中,验证基本功能。
- 无效等价类:空字段、错误联系方式、非法状态和损坏JSON,验证拒绝或降级处理。
- 边界数据:QQ长度、10MB图片及超限1字节、日期起止时刻,检查临界位置。
- 组合数据:准备全部满足及只差一个条件的记录,检查多个筛选条件是否同时生效。
- 异常与恢复:模拟保存、复制失败,检查旧数据;取消编辑和重新加载时检查状态是否保留。
2.2.3 组合条件与时间数据
搜索的一个容易遗漏的问题是:关键词匹配就返回了记录,却没有同时检查类型、分类和区域。UT23因此准备五条记录:
| 记录ID | 与基础记录的差异 | 按“耳机+寻物+电子设备+图书馆”搜索 |
|---|---|---|
| test-001 | 无,全部条件满足 | 返回 |
| found | 类型改为found | 不返回 |
| other-category | 分类改为钥匙 | 不返回 |
| other-region | 区域改为dorm | 不返回 |
| done | 状态改为resolved | 不返回 |
断言要求完整结果数组只包含test-001。如果误用OR连接条件,或遗漏某个条件,其他记录混入就会失败。
日期采用固定时刻构造,避免今天通过、过几天失败:
- UT26近7天:固定现在为2026-10-08 12:00;2026-10-01 12:00应包含,起点前一分钟和现在后一分钟应排除。
- UT27自定义日期:开始日零点、结束日23:59应包含,结束日次日零点应排除。
- 反向日期页面检查:开始2026-10-09、结束2026-10-01,页面应显示日期错误,并清空结果,而不是提供正常的无结果发布入口。
这里“近7天”按滚动时间计算,自定义日期按起止日期计算,规则不同,分别测试。
2.3 白盒测试设计
我们选择状态更新作为主要对象。阅读js/data.js后,发现 updateStatus() 先检查目标状态,再调用 updateOwnedRecord();后者通过 readOwnedRecord() 查找本人记录,然后保存到 localStorage。保存成功后才更新内存并通知页面。
因此,按照实际判断和执行顺序设计以下用例:
| 编号 | 构造的条件 | 预期结果 |
|---|---|---|
| UT10 | 本人寻物从active改为resolved | 返回值、存储和内存均更新 |
| UT11 | 本人已完成招领改回active | 恢复为进行中 |
| UT12 | 目标状态为wrong | 抛出无效状态错误,存储不变 |
| UT13 | 当前身份owner-a,记录属于owner-b | 拒绝修改,不执行保存,旧数据不变 |
| UT14 | 编号为missing-id | 找不到本人记录,抛出错误 |
| UT15 | 缺少当前身份标记 | 拒绝修改 |
| UT16 | 模拟setItem抛出异常 | 原存储与内存不变,不发送更新事件 |
以UT13为例,我们保留记录编号test-001,只把发布者改成owner-b,当前身份仍为owner-a。这样可以进入“无法找到本人记录”的错误路径。测试不仅检查抛出错误,还检查setItem没有被调用,确认权限判断确实阻止了保存。
UT16则模拟身份合法但保存失败的情况,具体代码见本节“3.2 保存失败时保持原数据”。UT10和UT11验证同一成功路径下的两种状态转换,不将它们当作两个独立判断分支。本组验证了上述代表性路径,未统计全项目分支覆盖率。
2.4 代表性测试用例
以下22条为50个自动化用例中的部分展示,编号与测试代码一致。预期先根据需求和代码分支确定,实际结果依据本次运行记录。
| 编号 | 被测函数或接口 | 输入及条件 | 预期结果 | 设计方法 | 本次结果 |
|---|---|---|---|---|---|
| UT01 | statusText | lost、active | 寻找中 | 白盒返回路径 | 通过 |
| UT02 | statusText | found、active | 等待认领 | 白盒返回路径 | 通过 |
| UT03 | statusText | lost、resolved | 已找到 | 白盒返回路径 | 通过 |
| UT04 | statusText | found、resolved | 已归还 | 白盒返回路径 | 通过 |
| UT05 | displayTime | 空字符串 | 时间未填写 | 空值等价类 | 通过 |
| UT06 | displayTime | 2026-10-08T10:30:59 | 2026-10-08 10:30 | 正常等价类 | 通过 |
| UT08 | itemPhotos | 非法来源与四张合法图片混合 | 过滤非法数据,最多保留三张 | 无效等价类、数量边界 | 通过 |
| UT09 | createItem | 完整信息、已有本人标记 | 创建active记录,保存并加入列表 | 正常业务 | 通过 |
| UT10 | updateStatus | 本人、存在记录、resolved | 内存与存储均更新 | 白盒成功路径 | 通过 |
| UT11 | updateStatus | 本人、已完成招领、active | 恢复进行中 | 状态转换 | 通过 |
| UT12 | updateStatus | 状态wrong | 抛出无效状态错误,存储不变 | 白盒非法参数分支 | 通过 |
| UT13 | readOwnedRecord调用链 | 当前身份与发布者不同 | 拒绝且不执行保存 | 白盒权限分支 | 通过 |
| UT14 | updateStatus | missing-id | 抛出记录不存在错误 | 白盒记录分支 | 通过 |
| UT15 | readOwnedRecord调用链 | 缺少身份标记 | 拒绝修改 | 白盒身份分支 | 通过 |
| UT16 | updateOwnedRecord调用链 | setItem抛出异常 | 旧状态、存储及更新事件均保持原约束 | 异常路径 | 通过 |
| UT17 | deleteItem | 本人已有记录 | 存储和内存列表均移除 | 正常业务 | 通过 |
| UT18 | 数据初始化(readSavedItems) | 无法解析的JSON | 加载三条示例信息,不阻止初始化 | 数据异常 | 通过 |
| UT23 | filterItems | 同时设置关键词、类型、分类和区域 | 只返回全部条件满足且未完成的信息 | 条件组合 | 通过 |
| UT24 | filterItems | 不存在的关键词 | 空数组 | 无匹配等价类 | 通过 |
| UT26 | filterItems | 七天起点、起点前一分钟、未来一分钟 | 包含起点,排除后两类 | 时间边界 | 通过 |
| UT27 | filterItems | 起始零点、结束日23:59、次日零点 | 包含前两条,排除次日 | 日期边界 | 通过 |
| UT29 | sortSearchItems | 精确名称匹配与部分匹配 | 精确匹配优先,原数组不变 | 排序及副作用检查 | 通过 |
2.5 异常操作与边界情况
考虑测试人员可能不按正常步骤操作,我们没有只准备能够成功发布的数据,而是从以下三个方向设计检查:
- 故意输入错误内容:逐个清空必填项,填写格式错误的联系方式,使用不存在的关键词或反向日期,检查系统能否拒绝无效操作并给出对应提示。
- 尝试突破限制:修改其他身份的记录、使用不存在的编号、上传刚好达到大小上限及超过上限的图片,检查归属判断和边界限制是否生效。
- 在操作中制造失败:模拟存储写入失败和复制权限受限,检查是否出现“提示成功但没有保存”,以及原数据、表单内容是否被错误清除。
具体数据和检查方式如下:
| 可能的操作 | 实际构造方法 | 检查内容 |
|---|---|---|
| 必填项不填 | 完整表单中依次清空8个字段并派发submit事件 | 本人记录数仍为0,对应错误提示显示 |
| 联系方式乱填 | 电话1380013800、邮箱name@、QQ以0开头、微信1abc | 返回格式错误提示 |
| 卡住图片大小边界 | size为10MB及10MB加1字节 | 边界进入模拟压缩,超限被拒绝 |
| 损坏已保存的数据 | 向项目存储写入字符串{broken | 初始化不崩溃,加载三条示例 |
| 越权修改或访问不存在ID | owner-b、missing-id及缺失身份 | 返回错误,不能误改其他记录 |
| 保存时出错 | 用vi.spyOn模拟setItem抛异常 | 旧存储、内存和表单内容按用例保留 |
| 复制权限受限 | 模拟Clipboard拒绝,备用复制成功或失败 | 返回对应结果,临时文本框被移除 |
| 编辑过程中取消 | 新发布草稿为“未发布的雨伞”,已有记录为“黑色耳机” | 取消后恢复雨伞草稿,耳机原记录不变 |
| 重复搜索或浏览 | 写12条历史,再输入带空格的物品5;重复查看两次 | 历史最多10条、去重置顶;浏览计数为2 |
这些检查不仅看有没有报错,还检查旧数据有没有被改变、页面是否误报成功。例如发布保存失败时,测试检查本人记录仍为0、物品名称仍在表单、错误提示可见、成功页未打开。
3. 代表性测试代码与说明
3.1 正常更新状态
以下片段摘自实际测试文件,公共导入和环境清理代码省略。owned() 使用当前身份 owner-a 和属于该身份的基础记录建立独立环境;KEYS.items 对应项目存储键,item() 用于生成有效记录,ids() 用于提取结果编号。
测试目的:调用 ShiguangData.updateStatus() 完成本人寻物信息,检查返回值、存储和内存是否同步。
it("UT10:本人可以完成寻物信息", () => {
const data = owned();
expect(data.updateStatus("test-001", "resolved").status)
.toBe("resolved");
expect(JSON.parse(app.w.localStorage.getItem(KEYS.items))[0].status)
.toBe("resolved");
expect(data.getItems().find(x => x.id === "test-001").status)
.toBe("resolved");
});
被测函数:ShiguangData.updateStatus(id, status)。id是信息编号,status是目标状态,本项目使用active和resolved。
断言含义:第一项检查接口返回状态,第二项检查持久化内容,第三项检查当前内存列表。三项同时满足,才能确认修改没有只停留在某一层。
3.2 保存失败时保持原数据
测试目的:检查保存过程中发生异常时,是否保留原有数据,并避免通知页面更新成功。
it("UT16:保存失败时原状态和内存保持不变,且不发送成功更新事件", () => {
const data = owned();
const before = app.w.localStorage.getItem(KEYS.items);
const changed = vi.fn();
app.w.document.addEventListener("shiguang:datachange", changed);
vi.spyOn(app.w.Storage.prototype, "setItem")
.mockImplementation(() => {
throw new Error("模拟存储已满");
});
expect(() => data.updateStatus("test-001", "resolved"))
.toThrow("模拟存储已满");
expect(app.w.localStorage.getItem(KEYS.items)).toBe(before);
expect(data.getItems().find(x => x.id === "test-001").status)
.toBe("active");
expect(changed).not.toHaveBeenCalled();
});
被测函数:updateStatus()调用的updateOwnedRecord()保存路径。
模拟方式:将模拟浏览器Storage.prototype.setItem替换为抛出异常的函数,稳定复现存储不可用,不需要真的填满浏览器空间。
断言含义:检查异常能够传回、旧存储不变、内存仍为active、更新事件未触发。测试通过表示失败处理符合预期,并非保存成功。
3.3 自定义日期边界
测试目的:检查开始当天和结束当天的信息能够保留,次日的信息被排除。
it("UT27:自定义日期包含开始和结束当天,排除次日", () => {
const source = [
item({ id: "start", eventTime: "2026-10-01T00:00" }),
item({ id: "end", eventTime: "2026-10-08T23:59" }),
item({ id: "next", eventTime: "2026-10-09T00:00" })
];
const result = search.filterItems(source, "", "all", "", {
days: "custom",
startDate: "2026-10-01",
endDate: "2026-10-08"
});
expect(ids(result)).toEqual(["start", "end"]);
});
被测函数:filterItems()的自定义时间筛选分支。source是待筛选数组,空关键词和all类型用于排除其他条件影响。
数据设计:构造开始日零点、结束日23:59和次日零点三条记录,分别命中起点、终点当天和范围外。
断言含义:比较完整ID数组,既检查该留下的信息存在,也检查范围外记录没有混入。
4. 测试运行结果
以下截图来自本机实际执行结果。命令及运行方式见第 1 节;本节说明截图验证的内容。
4.1 全部测试结果
- 运行范围:执行
npm test,运行四个测试文件。 - 实际结果:公共工具 8 个、数据模块 13 个、搜索模块 9 个、业务流程 20 个,共 50 个用例全部通过。
- 结果解读:截图显示
Test Files 4 passed (4)和Tests 50 passed (50);本次耗时约 2.71 秒,耗时随电脑状态变化,不作为固定性能指标。

4.2 重点模块结果
4.2.1 数据模块
- 运行范围:单独执行
tests/data.test.js,使用详细输出展示 UT09—UT21。 - 实际结果:13 个用例全部通过,包括创建、身份校验、状态更新、删除、草稿、历史记录和浏览计数。
- 重点检查:UT13 验证非发布者不能修改,且没有执行保存;UT16 验证保存失败后原存储和内存不变,也不发送更新事件。

4.2.2 搜索模块
- 运行范围:单独执行
tests/search.test.js,使用详细输出展示 UT22—UT30。 - 实际结果:9 个用例全部通过,包括关键词、组合条件、区域归并、日期边界及排序。
- 重点检查:UT23 验证多个筛选条件必须同时满足;UT26、UT27 分别验证近 7 天和自定义日期的边界。

上述数据模块的 13 个用例和搜索模块的 9 个用例均包含在总计 50 个用例中,不重复计数。
5. 测试评价与改进
5.1 测试效果
- 用例数量与设计方法:共设计 50 个自动化用例,满足至少 10 个的要求。根据代码中的判断和执行路径进行白盒测试设计,并结合等价类划分、边界值分析及多条件组合构造数据。
- 核心功能检查:涉及信息发布、搜索筛选、详情展示、联系方式复制和状态更新。状态修改同时核对返回值、存储及内存,搜索比较完整结果数组,避免只检查某一处显示正确。
- 异常处理检查:针对非法状态、非本人修改、损坏存储、保存失败和复制失败等情况,既检查错误处理,也检查原数据是否保留、是否误触发成功更新事件。
- 自动化执行:支持通过
npm test批量运行,也支持开发过程中监听文件变化后重新测试,方便修改代码后检查原有功能。
本次运行的 50 个用例全部通过,能够为主要业务规则和部分页面联动提供验证依据。不过,用例通过只说明已检查的场景符合预期,不能据此认为程序不存在其他问题。
5.2 现有局限
- 包含不同层次的测试:公共工具、数据和搜索以函数级测试为主,
workflows.test.js还包含多个模块共同参与的 DOM 集成测试,因此不能把全部用例都视为独立的函数单元测试。 - 未统计代码覆盖率:本次没有生成覆盖率报告,无法确定全项目的语句或分支覆盖比例。用例数量较多,也不等于所有判断路径都已验证。
- 部分行为通过模拟检查:图片解码、Canvas 和剪贴板的部分行为依赖模拟。例如,图片尺寸缩放测试可以检查计算和调用是否正确,但不能代替真实图片的压缩、展示及浏览器复制权限检查。
- 验证范围有限:身份检查针对当前浏览器中的发布者标记,不属于账号认证;站外联系和物品交接也不在自动化测试范围内。
5.3 后续改进
- 保留回归测试:修改功能后先运行对应模块的测试,再执行
npm test检查全部用例,确认没有影响原有功能。 - 针对问题补充用例:后续新增功能或发现遗漏情况时,为该场景增加测试。问题修复后保留对应用例,防止相同问题再次出现。
- 结合真实浏览器检查:涉及图片、复制和页面交互的修改,在 Chrome 中补充实际操作,检查模拟环境无法完整验证的行为。
- 逐步补充覆盖率统计:后续可引入覆盖率工具,辅助查找未执行的代码分支,再根据业务重要性补充测试,而不是单纯增加用例数量。
七、GitHub 代码签入与 PR 记录
1. commit截图


2. PR记录截图



八、遇到的问题与解决方法
本次开发中,对进度影响较大的问题是共享样式文件的合并冲突,以及单元测试环境无法正常运行。两者分别涉及结对协作和测试工具学习,具体处理过程如下。
1. 共享样式文件重复修改,导致合并冲突
问题描述
我们在不同电脑上开发,虽然按功能分工,但首页、搜索页和发布页共用 css/style.css,双方仍会修改同一文件。
队友提交 PR 后,GitHub 提示:
This branch has conflicts that must be resolved
当时队友新增了发布和“我的发布”功能,我调整了搜索页样式。双方修改涉及相同代码区域,Git 无法自动合并。如果直接采用其中一方的文件,可能丢失另一方的样式修改。
做过哪些尝试
-
调整合并顺序:暂时回到搜索页样式调整之前的版本,先合入队友功能,再执行
git cherry-pick 249f5d7恢复自己的样式提交。但css/style.css再次出现冲突,说明调整顺序并不能消除同一区域的修改冲突。 -
逐段处理冲突:对比冲突区域,保留队友功能需要的样式,同时恢复搜索页布局,删除冲突标记后执行:
git add css/style.css git cherry-pick --continue -
缩小共享文件范围:后续整理代码时,将公共样式与页面样式拆分,分别放入
common.css、search.css、publish.css、mine.css等文件,减少修改集中在同一文件的情况。
是否解决
已完成冲突处理,队友功能和搜索页样式保留在合并后的代码中。文件拆分减少了后续冲突范围,但公共布局仍需要双方协调修改。
有何收获
结对开发除了明确“谁负责哪个功能”,还应明确共享文件的修改范围。以后开发前先同步主仓库,修改公共样式前提前沟通,提交时保持内容集中。处理冲突时,应根据双方修改的目的逐段合并,不能直接用一方文件覆盖另一方。
2. 单元测试环境配置失败,测试无法执行
问题描述
配置 Vitest 后,首次执行 npm test,终端提示:
No test files found, exiting with code 1
补上测试文件后,又在读取业务源码时出现:
TypeError: The URL must be of scheme file
这两个问题都发生在执行断言之前:前者没有找到测试文件,后者无法加载源码,需要先解决测试环境问题,才能检查业务函数。
做过哪些尝试
-
检查文件匹配规则:根据 Vitest 输出的规则创建
tests/utils.test.js,先只测试statusText(),减少排查范围。 -
定位源码读取错误:根据报错行,发现问题出在
readFileSync()使用的new URL(..., import.meta.url)。改用项目根目录定位源码:import { readFileSync } from "node:fs"; import { resolve } from "node:path"; const source = readFileSync( resolve(process.cwd(), "js", "utils.js"), "utf8" ); -
从最小用例逐步扩展:先验证进行中的寻物是否显示“寻找中”,确认环境能够运行后,再补充数据更新、搜索筛选、表单和异常处理测试。
是否解决
已解决。第一个状态文字用例运行通过,随后扩展为四个测试文件、50 个自动化用例,本次运行全部通过,结果见单元测试部分截图。
有何收获
测试失败时,应区分文件识别、源码加载、断言和业务逻辑问题,不能看到失败就直接修改业务代码。先运行一个最小用例,确认环境正常后再扩大测试范围,更容易定位错误;后续项目也应尽早搭建测试环境,避免在提交前集中处理配置问题。
九、队友评价
1. 值得学习的地方
这次合作下来,我觉得刘谦益做得比较好的是,会把功能后面的操作也考虑进去。比如发布信息之后怎么编辑,物品找回后怎么标记,标记错了又怎么撤销,他都做了相应处理。这也提醒我,写功能不能只看它能不能成功运行,还要想想用户填错了、点错了以后怎么办,把这些情况考虑进去,功能才更完整。他后面也花了不少时间调整页面布局、背景和图片,让首页、搜索页和发布页的风格更统一。两个人分别做的时候,很容易只顾自己负责的部分,单独看没什么问题,放在一起却不太协调。他后期的调整让整个网页看起来更像一个完整的项目,这种从整体上检查和完善页面的习惯值得我学习。
2. 需要改进的地方
我觉得我们还需要多沟通公共文件的修改。虽然一开始分好了功能,但实际开发时还是会改到同一个页面和样式文件,这次就碰到了 css/style.css 的合并冲突。这个是我们两个人都需要注意的地方,不能觉得各自负责不同功能,就不会互相影响。下次修改公共文件前可以先说清楚准备改哪里、会影响哪些页面,提交时也把主要变化写得具体一些。同时及时同步代码,避免各自积累了很多修改以后才一起合并。合并完成后,再按照发布、搜索、详情和状态更新的流程检查一遍,能更早发现两个模块接在一起时的问题。
十、项目开发总结
这次作业让我体会到,从原型到能用的网页,还有很多细节需要处理。除了页面跳转,还要考虑数据保存、错误提示,以及修改后其他页面能否同步更新。我们先完成“发布—搜索—详情—联系—更新状态”的基本流程,再加入组合筛选、图片上传、草稿保存和相似信息提醒。这个过程中,我逐渐意识到,新增功能不仅要能用,还要考虑它与已有功能是否衔接。
结对协作和测试也给我带来了不少收获。两个人分好功能后,仍会修改到相同文件,样式冲突说明我们需要更及时地沟通和同步代码。测试则从一个状态文字用例逐步扩展到 50 个自动化用例,让我开始关注保存失败、日期边界等正常操作中不容易发现的问题,也认识到“演示成功一次”还不足以说明功能可靠。
时间安排是这次需要改进的地方。预估耗时为 1260 分钟,实际记录为 1800 分钟,页面调整、附加功能和测试都比预计更费时。下次我会先确定核心功能和页面规范,再安排扩展,并及时记录问题和耗时。


浙公网安备 33010602011771号