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)前期核心功能开发记录

前期 GitHub 代码签入记录

(2)后期功能完善与项目收尾记录

后期 GitHub 代码签入记录

三、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 质量,减少保存的数据量。

成果展示:

屏幕截图 2026-10-10 010448
屏幕截图 2026-10-10 010501

6.2 多条件组合筛选

设计意义: 用户有时只记得地点或物品类别,单纯关键词搜索难以快速缩小范围。项目增加寻物/招领类型、八类物品、地点和状态组合筛选。

实现思路: 不同条件都通过第五节的 filterItems() 进行判断,避免多个页面分别实现筛选规则。八类物品为:证件卡类、数码电子、学习用品、生活用品、衣物配饰、钥匙、雨具、其他。

例如可筛选:寻物 + 数码电子 + 图书馆 + 进行中。对应筛选逻辑和效果已在第五节展示。

6.3 已完成信息归档

设计意义: 已找到、已归还的信息继续占据首页,会增加无效浏览与重复联系。

实现思路: 首页只展示进行中信息,已完成记录保留在历史记录,不直接删除;必要时可以在“我的发布”恢复为进行中。

关键代码:

const activeItems = Core.filterItems(
  items,
  { status: "active" }
).sort(
  (a, b) => b.createdAt - a.createdAt
);

在状态更新后重新执行筛选,记录便会进入对应列表。

屏幕截图 2026-10-10 014734

6.4 其他便捷功能

详情页提供一键复制联系方式,并准备兼容复制方式;“我的发布”支持删除错误信息和确认操作;右上角的恢复示例数据便于课程演示与重复测试。


七、页面效果展示

页面采用统一的简约粉色风格,使用圆角卡片、柔和渐变和固定底部导航。以下以最终页面截图展示主要布局,具体业务逻辑不再重复说明。

首页: 左侧搜索与快捷发布,右侧进行中信息列表独立滚动。

屏幕截图 2026-10-10 010830

搜索页: 筛选条件保持可见,结果区独立滚动。

屏幕截图 2026-10-10 010911

发布页: 寻物/招领共用表单,照片为可选项。

屏幕截图 2026-10-10 010944

详情页: 展示状态、物品信息、联系方式与照片。

屏幕截图 2026-10-10 011023

我的发布: 可查看和维护当前浏览器中的个人发布。

屏幕截图 2026-10-10 011101

八、项目目录与运行说明

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 运行方法

  1. 打开 GitHub 主仓库。
  2. 点击 Code → Download ZIP,解压后进入项目目录。
  3. 用 Google Chrome 直接打开 index.html,无需启动服务器或安装前端依赖。
  4. 可以依次测试“发布 → 搜索 → 详情 → 联系 → 我的发布中更新状态 → 历史记录”;需要重复演示时使用页面右上角恢复按钮。

项目数据保存在当前浏览器 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

项目使用说明:

README.md

GitHub 协作说明:

docs/GIT_GUIDE.md

单元测试报告:

docs/TEST_REPORT.md

人工测试记录:

docs/MANUAL_TESTS.md

自动化单元测试代码:

tests/core.test.js

posted @ 2026-10-09 15:26  陈瑀涵  阅读(18)  评论(0)    收藏  举报