软件工程结对作业(第二次之程序实现)

软件工程结对作业(第二次之程序实现)

项目 内容
这个作业属于哪个课程 202601 软件工程
这个作业要求在哪里 2026秋软件工程结对作业(第二次之程序实现)
这个作业的目标 将校园失物招领原型实现为可运行的 Web 应用,完成核心业务流程,并通过 GitHub 协作、PSP 和单元测试实践结对开发、版本管理与软件测试。
设计原型稿与在线演示 Figma 设计稿 , Figma 在线演示
GitHub 仓库 102401310-102401312
结对成员 102401310 庄凯堃、102401312 刘谦益
本篇博客 庄凯堃博客
队友博客 刘谦益博客

一、项目简介

1. 项目背景

校园里的失物招领信息常分散在不同群聊中,容易被新消息覆盖,查找和跟进都不方便。我们基于第一次结对作业的原型设计,实现了“拾光校园”失物招领网页,将寻物和招领信息集中展示,方便同学发布信息、查找线索和联系发布者。

2. 核心功能

项目围绕以下流程展开:

发布信息 → 浏览或搜索 → 查看详情 → 联系发布者 → 更新状态

  1. 发布信息:选择寻物或招领,填写物品特征、时间、地点及联系方式。
  2. 浏览或搜索:浏览最新信息,或通过关键词查找相关记录。
  3. 查看详情:核对物品描述、照片、时间、地点和当前状态。
  4. 联系发布者:查看并复制联系方式,通过对应渠道在站外沟通。
  5. 更新状态:物品找回或归还后,由发布者在“我的”中标记“已找到”或“已归还”,减少无效联系。

在基本流程之外,还增加了组合筛选、自定义日期范围、相似信息提醒、图片上传、草稿保存和搜索历史等便利功能。

二、具体分工与合作情况

1. 具体分工

两人按功能模块分工,共同完成需求讨论、接口约定和功能联调。

成员 主要负责内容
庄凯堃 搜索与详情:关键词搜索、分类与区域筛选、时间范围筛选、排序、搜索历史、详情展示及联系方式复制。
扩展功能:图片上传与展示、草稿保存与恢复、相似信息提醒、联系方式格式校验及本人发布删除。
测试与文档:编写和运行自动化测试,整理 README 及个人博客材料。
刘谦益 发布与管理:基础寻物与招领发布、“我的发布”、记录编辑、完成标记与撤销、状态筛选与统计。
页面联动:首页和搜索结果隐藏已完成信息。
视觉调整:首页、搜索页和发布页的布局优化,统一页面版式及图片资源。

2. 协作过程

2.1 共同工作

环节 共同完成的内容
需求分析 根据原型确定核心功能,梳理“发布—浏览或搜索—详情—联系—更新状态”的使用流程,划分开发任务。
接口约定 统一物品字段、信息类型、状态及本地存储方式,明确各模块的数据读取和更新规则。
功能联调 检查发布、编辑、完成、恢复和删除操作能否同步更新首页、搜索结果及“我的发布”。
交叉检查 检查对方负责的模块,结合正常操作和异常输入排查问题,调整交互及页面显示。
材料整理 汇总实现思路、开发问题、测试结果和演示材料,完成作业博客。

2.2 GitHub 协作方式

  1. 分别开发:庄凯堃维护主仓库,刘谦益 fork 仓库,在各自电脑上完成负责的模块。
  2. 提交与合并:功能完成后提交代码,刘谦益通过 Pull Request 提交修改,检查双方改动并处理冲突后合入主仓库。
  3. 同步与联调:将主仓库的最新代码拉取到本地,再检查模块之间的衔接和完整流程。

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. 关键实现的流程图

发布与状态管理的数据流图

图中重点展示发布、编辑和状态修改如何影响其他页面。用户操作交给数据层处理后,程序按照以下顺序执行:

校验输入或记录归属 → 写入本地存储 → 更新内存数据 → 通知相关页面重新渲染。

这里有两个关键处理:

  1. 保存成功后才更新页面数据:如果写入失败,后续内存更新和成功通知不会执行,调用页面显示错误,避免界面显示成功但刷新后修改丢失。
  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")
  );
}

这段代码是编辑信息和更新状态共用的保存入口,处理顺序如下:

  1. 读取并核对记录:readOwnedRecord() 从存储中读取最新记录,并检查它是否属于当前浏览器中的发布者。
  2. 合并修改内容:通过展开运算符合并原记录与修改字段,保留其他信息。
  3. 先保存,再更新:localStorage.setItem() 成功后才替换内存记录。若写入抛出异常,后续操作停止,由调用页面处理错误。
  4. 通知页面刷新:派发统一事件,使相关列表重新读取数据并渲染。

将这些步骤集中处理,可以避免编辑和状态修改各自维护一套保存逻辑,也能减少存储、内存和页面显示不一致的问题。

代码片段二:根据关键词相关性与发布时间排序

代码来源: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() 得到符合条件的记录,再交给排序函数处理,避免把筛选与排序混在一起。

  1. 计算相关性:名称完全匹配得 4 分,名称包含关键词得 3 分,分类匹配得 2 分,描述或地点匹配得 1 分。例如搜索“钥匙”时,名称包含“钥匙”的记录优先于只在描述中提到“钥匙”的记录。
  2. 处理不同排序方式:默认按相关性排序,浏览次数排序则优先比较浏览量;相关性或浏览次数相同时,再按发布时间从新到旧排列。
  3. 处理缺失时间:有效发布时间优先于无效时间,避免异常数据影响正常记录的顺序。
  4. 避免修改原数组:使用 [...source] 创建副本后再调用 sort(),防止排序影响其他模块使用的数据顺序。

四、附加特色设计与展示

在完成“发布信息—浏览或搜索—查看详情—联系发布者—更新状态”的基本流程后,我们围绕三个使用问题进行扩展:发布前如何发现已有线索、搜索结果较多时如何缩小范围、填写中断后如何减少重复操作。

下面重点介绍相似信息提醒和组合筛选,并汇总其他便利功能。

特点一:发布时的相似信息提醒

1.1 设计意义

用户准备发布信息时,可能还没有查看已有记录。例如,一位同学准备发布捡到校园卡的招领信息,而失主已经发布了寻物启事,双方仍需要再次搜索才能发现对方。

因此,我们将线索匹配融入发布过程:用户填写名称或选择分类时,页面自动展示相关记录,并优先展示与当前发布类型相反的信息。发布招领时优先展示寻物,发布寻物时优先展示招领,方便用户在填写过程中发现联系线索。

相似结果只是候选信息,是否为同一件物品仍需用户核对。

1.2 实现思路

处理流程为:

整理输入 → 筛选候选记录 → 计算匹配分数 → 排序 → 展示结果。

  1. 整理输入:统一名称大小写,去除空白、标点和符号,减少输入形式差异。
  2. 筛选候选记录:排除已完成的信息和指定编号的记录。
  3. 计算匹配分数:根据名称相似程度计分,分类相同则额外加分。
  4. 排列结果:相反类型优先,其次比较匹配分数和发布时间,最多展示三条。
  5. 连接详情页:点击候选信息后进入详情,进一步核对特征及联系方式。

匹配规则如下:

匹配情况 计分规则
名称完全相同 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 实现成果展示

发布时的相似信息提醒演示

展示流程:

  1. 在发布页输入物品名称或选择分类。
  2. 查看页面提供的候选信息。
  3. 点击候选记录进入详情,核对物品特征及联系方式。

特点二:搜索时的多条件组合筛选

2.1 设计意义

关键词搜索能够找到相关信息,但“校园卡”“钥匙”等常见物品可能对应多条结果。用户通常还记得地点或时间,例如“最近几天在图书馆丢失的校园卡”,仅靠关键词难以利用这些线索。

因此,我们在关键词搜索基础上增加类型、分类、区域和时间筛选,支持多个条件同时使用。时间还支持自定义日期范围,方便用户查找某段时间内的信息。

用户可以根据已有线索逐步缩小结果范围,减少逐条打开详情的操作。

2.2 实现思路

处理流程为:

整理关键词 → 排除已完成信息 → 组合条件筛选 → 排序 → 更新结果。

  1. 匹配关键词:去除首尾空白、统一大小写,在名称、分类、描述和地点中匹配。
  2. 组合筛选条件:记录必须满足全部已选条件;未选择的条件不限制结果。
  3. 处理时间范围:近 7 天、近 30 天按当前时间计算,自定义范围包含开始和结束日期当天。
  4. 兼容旧数据:优先使用区域字段,旧记录缺少该字段时再通过地点文字判断,例如将实验楼归入教学区。
  5. 更新结果:刷新卡片和数量,无匹配记录时展示提示及“发布寻物”入口。

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 实现成果展示

搜索组合筛选与自定义日期演示

展示流程:

  1. 输入关键词,选择类型、分类和区域。
  2. 切换近 7 天、近 30 天或自定义日期范围。
  3. 观察结果数量变化,并通过排序调整展示顺序。

其他便利特点

除以上两项重点特色外,项目还实现了以下扩展:

功能 实现方式与作用
草稿自动保存与恢复 自动保存未提交的表单内容和图片,刷新后可继续填写;支持主动清除,发布成功后自动清除。编辑已有记录时不覆盖新发布草稿。
图片上传与放大查看 最多上传三张 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                 # 项目说明

目录组织遵循以下原则:

  1. 页面集中组织:各视图写在 index.html 中,由 app.js 控制切换。
  2. 样式和功能按职责拆分:公共样式与各页面样式分开,JavaScript 按业务模块组织。
  3. 公共逻辑统一复用:数据由 data.js 管理,卡片由 card.js 生成,时间格式和复制等工具集中在 utils.js。
  4. 资源和文档独立存放:图片及图标许可放在 assets/,开发记录放在 docs/,运行说明写入 README。

网页使用原生 HTML、CSS 和 JavaScript,没有运行时第三方框架依赖。本地单元测试另外使用 tests/、package.json 和 vitest.config.mjs,正常使用网页不需要安装测试依赖。

2. 运行方式

推荐使用电脑端 Google Chrome。项目不需要安装依赖、配置数据库或启动后端。

  1. 在 GitHub 仓库 中点击 Code → Download ZIP,下载并解压项目。
  2. 进入包含 index.html、assets/、css/ 和 js/ 的项目根目录。
  3. 使用 Chrome 打开 index.html,即可使用。

请保留完整目录,不要只复制 index.html。修改代码后,可按 Ctrl + F5 刷新页面。

3. 使用说明

基本流程:发布信息 → 浏览或搜索 → 查看详情 → 联系发布者 → 更新状态。

3.1 发布信息

  1. 选择类型:进入“发布”页面,丢失物品选择“寻物”,捡到物品选择“招领”。
  2. 填写信息:填写物品名称、分类、丢失或拾取时间、区域、具体地点、外观特征,以及联系渠道和联系方式。
  3. 上传照片:可选,最多三张,支持 JPG、PNG、WebP,单张原图不超过 10 MB;未上传时显示默认分类图案。
  4. 查看相似信息:填写过程中若出现相似信息,可先查看是否与自己的物品有关。
  5. 提交发布:点击“发布信息”。必填项缺失或联系方式格式不正确时,按照提示修改;发布成功后,可查看详情。
  6. 管理草稿:未提交的内容会自动保存,再次进入可继续填写;点击“清除草稿”可重新开始,成功发布后草稿自动清除。

发布成功后,点击“查看我的发布”,再点击对应物品卡片进入详情页

3.2 浏览或搜索

  1. 浏览首页:首页展示最新的三条有效信息,点击“查看全部”进入搜索页。

  2. 输入关键词:输入物品名称、外观特征或地点,点击搜索按钮或按回车键。

  3. 筛选结果:

    筛选项 可选内容
    信息类型 全部、寻物、招领
    物品分类 证件卡片、钥匙、电子设备等
    所在区域 教学区、图书馆、食堂、宿舍区、操场等,教学区包含实验楼
    时间范围 全部时间、近 7 天、近 30 天、自定义起止日期,依据丢失或拾取时间筛选
  4. 调整排序:选择默认排序、发布时间或浏览次数。

  5. 使用搜索记录:点击搜索框,在下拉列表中选择历史关键词,也可以删除单条记录或清空记录。

  6. 处理无结果情况:调整关键词或筛选条件;需要留下寻找信息时,点击“发布寻物”。

3.3 查看详情

  1. 打开信息:点击物品卡片进入详情页。
  2. 核对内容:查看物品名称、分类、外观特征、区域、具体地点、丢失或拾取时间、发布时间和当前状态。
  3. 查看照片:有照片时,点击放大查看。
  4. 查看浏览次数:卡片上的小眼睛表示浏览次数,每次打开详情都会计数,仅保存在当前浏览器中。

3.4 联系发布者

  1. 获取联系方式:在详情页查看联系渠道和联系方式。
  2. 复制并联系:点击复制按钮,通过对应的 QQ、微信、电话、邮箱等渠道联系发布者。
  3. 确认与交接:进一步核对物品特征,确认后约定交接方式。沟通和交接在网站外完成。

3.5 更新状态

  1. 找到本人记录:发布者进入“我的”页面,查看自己的发布信息,可按全部、进行中、已完成切换。
  2. 标记完成:物品找回后,将寻物标记为“已找到”;物品归还后,将招领标记为“已归还”。
  3. 查看完成记录:已完成的信息保留在“我的”页面,不再出现在首页和搜索结果中。
  4. 恢复状态:需要继续寻找或招领时,可恢复为进行中。
  5. 编辑或删除:信息有误时可编辑;不再需要时可删除,删除前需要确认。

项目没有账号登录,“我的发布”通过当前浏览器的身份标记识别,请使用发布时的浏览器和访问地址管理记录

4. 数据保存与重置

  1. 保存范围:发布信息、图片、草稿、搜索历史和浏览计数保存在当前浏览器站点的 localStorage 中。刷新后可以保留,不同电脑、浏览器或访问地址之间不会自动同步,上传 GitHub 也不会上传这些数据。
  2. 重置方法:按 F12,进入 Application → Local Storage → 当前项目地址,删除以 shiguang_ 开头的数据并刷新。该操作会清除当前站点的项目数据,并重新显示默认示例信息。
  3. 测试提醒:示例日期较早,测试“近 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等对象

选择这套工具主要考虑三点:

  1. 能测试现有代码:不要求把网页改成Vue或React,可以直接加载原来的JavaScript。
  2. 能检查失败情况:使用vi.spyOn()模拟保存失败,检查旧记录是否被破坏。
  3. 能重复运行:一条npm test运行全部用例,修改代码后可以再次检查。

1.2 学习与调试过程

学习时,我们先理解单元测试与回归测试的作用,再学习 describe、it 和 expect 的用法,随后配置 Vitest 环境,从项目中的一个实际函数开始编写和运行测试。单元测试用于检查具体函数的行为;回归测试则是在修改代码后重新执行已有用例,检查原有功能是否受到影响。

学习和调试过程分为四步:

  1. 从一个函数开始:先给statusText()传入lost、active,检查返回“寻找中”,理解“准备数据—调用函数—比较结果”。
  2. 解决文件发现问题:第一次npm test提示“No test files found”。创建tests/utils.test.js后,Vitest才找到测试文件。
  3. 解决源码读取问题:使用new URL(..., import.meta.url)读取源码时出现“The URL must be of scheme file”。改用resolve(process.cwd(), "js", "utils.js")定位文件后,第一个用例通过。
  4. 逐步扩展:补齐四种状态文字,再增加状态更新、搜索日期、存储失败和表单交互;最终运行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
  1. 批量执行:npm test 自动收集并运行测试文件,不需要手动逐条调用函数。
  2. 修改后复查:修改数据、搜索或发布逻辑后,再次执行全部测试,检查原有功能。
  3. 开发时重跑: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 数据分类

构造数据时分为以下几类:

  1. 正常数据:有效表单、本人更新、搜索命中,验证基本功能。
  2. 无效等价类:空字段、错误联系方式、非法状态和损坏JSON,验证拒绝或降级处理。
  3. 边界数据:QQ长度、10MB图片及超限1字节、日期起止时刻,检查临界位置。
  4. 组合数据:准备全部满足及只差一个条件的记录,检查多个筛选条件是否同时生效。
  5. 异常与恢复:模拟保存、复制失败,检查旧数据;取消编辑和重新加载时检查状态是否保留。
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 异常操作与边界情况

考虑测试人员可能不按正常步骤操作,我们没有只准备能够成功发布的数据,而是从以下三个方向设计检查:

  1. 故意输入错误内容:逐个清空必填项,填写格式错误的联系方式,使用不存在的关键词或反向日期,检查系统能否拒绝无效操作并给出对应提示。
  2. 尝试突破限制:修改其他身份的记录、使用不存在的编号、上传刚好达到大小上限及超过上限的图片,检查归属判断和边界限制是否生效。
  3. 在操作中制造失败:模拟存储写入失败和复制权限受限,检查是否出现“提示成功但没有保存”,以及原数据、表单内容是否被错误清除。

具体数据和检查方式如下:

可能的操作 实际构造方法 检查内容
必填项不填 完整表单中依次清空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 全部测试结果

  1. 运行范围:执行 npm test,运行四个测试文件。
  2. 实际结果:公共工具 8 个、数据模块 13 个、搜索模块 9 个、业务流程 20 个,共 50 个用例全部通过。
  3. 结果解读:截图显示 Test Files 4 passed (4) 和 Tests 50 passed (50);本次耗时约 2.71 秒,耗时随电脑状态变化,不作为固定性能指标。

全部自动化测试结果

4.2 重点模块结果

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

数据模块逐条结果

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

搜索模块逐条结果

上述数据模块的 13 个用例和搜索模块的 9 个用例均包含在总计 50 个用例中,不重复计数。

5. 测试评价与改进

5.1 测试效果

  1. 用例数量与设计方法:共设计 50 个自动化用例,满足至少 10 个的要求。根据代码中的判断和执行路径进行白盒测试设计,并结合等价类划分、边界值分析及多条件组合构造数据。
  2. 核心功能检查:涉及信息发布、搜索筛选、详情展示、联系方式复制和状态更新。状态修改同时核对返回值、存储及内存,搜索比较完整结果数组,避免只检查某一处显示正确。
  3. 异常处理检查:针对非法状态、非本人修改、损坏存储、保存失败和复制失败等情况,既检查错误处理,也检查原数据是否保留、是否误触发成功更新事件。
  4. 自动化执行:支持通过 npm test 批量运行,也支持开发过程中监听文件变化后重新测试,方便修改代码后检查原有功能。

本次运行的 50 个用例全部通过,能够为主要业务规则和部分页面联动提供验证依据。不过,用例通过只说明已检查的场景符合预期,不能据此认为程序不存在其他问题。

5.2 现有局限

  1. 包含不同层次的测试:公共工具、数据和搜索以函数级测试为主,workflows.test.js 还包含多个模块共同参与的 DOM 集成测试,因此不能把全部用例都视为独立的函数单元测试。
  2. 未统计代码覆盖率:本次没有生成覆盖率报告,无法确定全项目的语句或分支覆盖比例。用例数量较多,也不等于所有判断路径都已验证。
  3. 部分行为通过模拟检查:图片解码、Canvas 和剪贴板的部分行为依赖模拟。例如,图片尺寸缩放测试可以检查计算和调用是否正确,但不能代替真实图片的压缩、展示及浏览器复制权限检查。
  4. 验证范围有限:身份检查针对当前浏览器中的发布者标记,不属于账号认证;站外联系和物品交接也不在自动化测试范围内。

5.3 后续改进

  1. 保留回归测试:修改功能后先运行对应模块的测试,再执行 npm test 检查全部用例,确认没有影响原有功能。
  2. 针对问题补充用例:后续新增功能或发现遗漏情况时,为该场景增加测试。问题修复后保留对应用例,防止相同问题再次出现。
  3. 结合真实浏览器检查:涉及图片、复制和页面交互的修改,在 Chrome 中补充实际操作,检查模拟环境无法完整验证的行为。
  4. 逐步补充覆盖率统计:后续可引入覆盖率工具,辅助查找未执行的代码分支,再根据业务重要性补充测试,而不是单纯增加用例数量。

七、GitHub 代码签入与 PR 记录

1. commit截图

image-20261009105939160

image-20261009105956411

2. PR记录截图

image-20261009110059707

image-20261009110132467

image-20261009110201550

八、遇到的问题与解决方法

本次开发中,对进度影响较大的问题是共享样式文件的合并冲突,以及单元测试环境无法正常运行。两者分别涉及结对协作和测试工具学习,具体处理过程如下。

1. 共享样式文件重复修改,导致合并冲突

问题描述

我们在不同电脑上开发,虽然按功能分工,但首页、搜索页和发布页共用 css/style.css,双方仍会修改同一文件。

队友提交 PR 后,GitHub 提示:

This branch has conflicts that must be resolved

当时队友新增了发布和“我的发布”功能,我调整了搜索页样式。双方修改涉及相同代码区域,Git 无法自动合并。如果直接采用其中一方的文件,可能丢失另一方的样式修改。

做过哪些尝试

  1. 调整合并顺序:暂时回到搜索页样式调整之前的版本,先合入队友功能,再执行 git cherry-pick 249f5d7 恢复自己的样式提交。但 css/style.css 再次出现冲突,说明调整顺序并不能消除同一区域的修改冲突。

  2. 逐段处理冲突:对比冲突区域,保留队友功能需要的样式,同时恢复搜索页布局,删除冲突标记后执行:

    git add css/style.css
    git cherry-pick --continue
    
  3. 缩小共享文件范围:后续整理代码时,将公共样式与页面样式拆分,分别放入 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

这两个问题都发生在执行断言之前:前者没有找到测试文件,后者无法加载源码,需要先解决测试环境问题,才能检查业务函数。

做过哪些尝试

  1. 检查文件匹配规则:根据 Vitest 输出的规则创建 tests/utils.test.js,先只测试 statusText(),减少排查范围。

  2. 定位源码读取错误:根据报错行,发现问题出在 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"
    );
    
  3. 从最小用例逐步扩展:先验证进行中的寻物是否显示“寻找中”,确认环境能够运行后,再补充数据更新、搜索筛选、表单和异常处理测试。

是否解决

已解决。第一个状态文字用例运行通过,随后扩展为四个测试文件、50 个自动化用例,本次运行全部通过,结果见单元测试部分截图。

有何收获

测试失败时,应区分文件识别、源码加载、断言和业务逻辑问题,不能看到失败就直接修改业务代码。先运行一个最小用例,确认环境正常后再扩大测试范围,更容易定位错误;后续项目也应尽早搭建测试环境,避免在提交前集中处理配置问题。

九、队友评价

1. 值得学习的地方

这次合作下来,我觉得刘谦益做得比较好的是,会把功能后面的操作也考虑进去。比如发布信息之后怎么编辑,物品找回后怎么标记,标记错了又怎么撤销,他都做了相应处理。这也提醒我,写功能不能只看它能不能成功运行,还要想想用户填错了、点错了以后怎么办,把这些情况考虑进去,功能才更完整。他后面也花了不少时间调整页面布局、背景和图片,让首页、搜索页和发布页的风格更统一。两个人分别做的时候,很容易只顾自己负责的部分,单独看没什么问题,放在一起却不太协调。他后期的调整让整个网页看起来更像一个完整的项目,这种从整体上检查和完善页面的习惯值得我学习。

2. 需要改进的地方

我觉得我们还需要多沟通公共文件的修改。虽然一开始分好了功能,但实际开发时还是会改到同一个页面和样式文件,这次就碰到了 css/style.css 的合并冲突。这个是我们两个人都需要注意的地方,不能觉得各自负责不同功能,就不会互相影响。下次修改公共文件前可以先说清楚准备改哪里、会影响哪些页面,提交时也把主要变化写得具体一些。同时及时同步代码,避免各自积累了很多修改以后才一起合并。合并完成后,再按照发布、搜索、详情和状态更新的流程检查一遍,能更早发现两个模块接在一起时的问题。

十、项目开发总结

这次作业让我体会到,从原型到能用的网页,还有很多细节需要处理。除了页面跳转,还要考虑数据保存、错误提示,以及修改后其他页面能否同步更新。我们先完成“发布—搜索—详情—联系—更新状态”的基本流程,再加入组合筛选、图片上传、草稿保存和相似信息提醒。这个过程中,我逐渐意识到,新增功能不仅要能用,还要考虑它与已有功能是否衔接。

结对协作和测试也给我带来了不少收获。两个人分好功能后,仍会修改到相同文件,样式冲突说明我们需要更及时地沟通和同步代码。测试则从一个状态文字用例逐步扩展到 50 个自动化用例,让我开始关注保存失败、日期边界等正常操作中不容易发现的问题,也认识到“演示成功一次”还不足以说明功能可靠。

时间安排是这次需要改进的地方。预估耗时为 1260 分钟,实际记录为 1800 分钟,页面调整、附加功能和测试都比预计更费时。下次我会先确定核心功能和页面规范,再安排扩展,并及时记录问题和耗时。

614720EFAFB2915CA6DCE0C2A7BF5B6F

posted @ 2026-10-09 00:49  Ziekun  阅读(24)  评论(1)    收藏  举报