第二次结对作业

找呀找——校园失物招领 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 协作流程与开发过程

项目开展路线:搜索 → 详情 → 发布 → 状态管理 → UI 优化 → 图片功能 → 测试与文档 ,以下为 GitHub 代码签入记录截图
(1)核心功能开发(前期)
9863b8e4cf31f0f7454768d012f3f28d
(2)功能完善与补充(后期)
9f7f2bfdbe64141feafcfe7908c62ceb

三、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

分析结果:根据记录,项目预估耗时 1140 分钟,实际耗时 1660 分钟,增加了 520 分钟(约 45.6%)。其中编码环节增加 230 分钟,主要源于图片处理与多轮 UI 调整;测试环节增加 95 分钟,用于功能走查、问题修复及自动化测试编写。此外,GitHub 分支同步、PR 流程与账号认证也需要额外学习与排查。这次经历表明,项目计划不能只估算首次编码时间,还应为测试、界面返工和团队协作预留缓冲。

四、解题思路描述与设计实现说明

4.1代码实现思路

(1)模块划分——结构、逻辑、交互”三层分离

模块 文件 主要职责
页面与样式 index.html、styles.css、overrides.css 负责页面骨架、粉色视觉风格、横屏布局与响应式样式
业务逻辑 assets/js/core.js 负责搜索筛选、表单校验、信息创建与状态切换等纯逻辑处理
页面交互 assets/js/app.js 负责 Hash 导航、DOM 渲染、图片处理与 localStorage 读写

(2)基于 Hash 的视图切换
通过 location.hash 在首页、搜索、发布、详情、我的发布和历史记录等视图之间切换,无需启动服务器。

4.2 业务流程图

b443d936072a4ab21166472ff42b176d

4.3 核心功能及关键代码

(1)发布及内容校验
关键代码:

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 };
  }

当没有填写地址时,系统发出提示
image
完成填写内容校验,通过后可发布
image

(2)检索与筛选
关键代码:

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;
    });
}

系统会根据用户输入的关键词筛选并列出符合条件的物品
image
(3)状态更新
关键代码:

function toggleStatus(item) {
    if (!item || !["lost", "found"].includes(item.type)) return item;
    return Object.assign({}, item, { status: statusFor(item.type, !isCompleted(item)) });
  }

  function uniqueCategories(items) {
    return Array.from(new Set((items || []).map((item) => item.category).filter(Boolean))).sort();
  }

  return { normalize, createId, statusFor, isCompleted, filterItems, validateItem, buildItem, toggleStatus, uniqueCategories };
});

寻物和招领的两种状态可以来回切,切之前先弹窗确认,确认后马上存进本地,所有页面共用同一份数据,改一处、处处同步
(4)数据存储
关键代码:

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;
  }
}

五、附加特点设计与展示

5.1图片上传与本地预览

(1)设计意义:同类物品外观容易相似,照片可辅助失主识别细节,减少误认
(2)实现思路:如上传照片,最多支持 4 张;单张原图限制 2MB,用户可以预览、删除并重新选择。详情页按数量排列图片,支持放大查看。为了降低 localStorage 压力,在浏览器端使用 Canvas 缩小尺寸并压缩为 JPEG
(3)关键代码:

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
  );
}

效果展示:
snoopy

5.2 多条件组合筛选

(1)设计意义: 用户有时只记得地点或物品类别,单纯关键词搜索难以快速缩小范围。项目增加寻物/招领类型、八类物品、地点和状态组合筛选
(2)实现思路: 不同条件都通过第四节的检索与筛选进行判断,避免多个页面分别实现筛选规则。八类物品为:证件卡类、数码电子、学习用品、生活用品、衣物配饰、钥匙、雨具、其他
例如可筛选:寻物 + 帽子 + 图书馆,对应筛选逻辑和效果已在第四节展示

5.3已完成信息归档

(1)设计意义: 已找到、已归还的信息继续占据首页,会增加无效浏览与重复联系
(2)实现思路: 首页只展示进行中信息,已完成记录保留在历史记录,不直接删除;必要时可以在“我的发布”恢复为进行中
(3)关键代码:

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

效果展示:
image

六、完整项目展示

依次为:首页、详情页、搜索页、发布页和个人主页
横向原型拼接长图

七、项目目录与运行说明

7.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                   # 使用说明``

7.2运行方法

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

项目数据保存在当前浏览器 localStorage中,刷新后仍可保留;但清除浏览器数据或换设备不会同步。更多说明见 README.md。

八、单元测试与测试分析

8.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

8.2 白盒测试与测试数据设计

根据 core.js 的条件分支设计测试,兼顾正常、异常和边界情况。搜索测试覆盖命中/不命中、单条件/组合条件;发布校验覆盖空字段、描述过短、非法联系方式、合法手机号与邮箱;状态测试覆盖寻物与招领的双向状态;图片测试覆盖 0、1、4、5 张及旧单图数据兼容。

共编写 25 个自动化单元测试,主要分布如下:

编号 测试范围 主要检查
01–06 文本与搜索 标准化、关键词、类型、分类、条件冲突
07–11 发布校验 必填项、描述长度、手机号和邮箱
12–16 信息与状态 默认状态、状态切换、分类去重
17–19 状态筛选 进行中、已完成及组合查询
20–25 图片边界 旧字段兼容、0/1/4/5 张、非法数据

8.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]
    );
  }
);

8.4 测试结果与评价

自动化单元测试共 25 项,通过 25 项,失败 0 项。另外在 Chrome 中按照 docs/MANUAL_TESTS.md 进行 25 项人工走查,覆盖发布、搜索无结果、复制联系方式、状态更新、图片预览、存储与不同分辨率布局等场景。

我们重点考虑测试人员可能遇到的异常输入、图片超限、刷新后数据是否保留、直接通过 file:// 打开网页等情况。单元测试主要验证纯逻辑,人工测试补充页面交互;目前尚未量化代码分支覆盖率,也未实现完整的浏览器端自动化测试。

详细测试过程可见 测试报告 和 人工测试记录。

九、开发中遇到的问题及解决方法

9.1 页面布局与原型不一致

问题: 首页比例、按钮位置与信息列表滚动效果需要反复调整。
尝试: 检查 HTML 容器关系,在 Chrome 中逐轮调整 CSS 布局和高度,并使用 overrides.css 集中处理。
结果: 最终形成较稳定的粉色双栏与局部滚动布局。
收获: 页面不仅要好看,也要在真实交互和不同窗口尺寸下可用。

9.2 图片占用本地存储空间

问题: 多张原始图片容易达到 localStorage 容量上限。
尝试: 限制单图 2MB、最多 4 张,使用 Canvas 缩放压缩,并处理保存失败提示。
结果: 降低了存储压力,避免保存失败却显示成功。
收获: 新功能需要同时设计异常与失败处理。

十、对队友的评价

评价对象:102402107 陈瑀涵

值得学习的地方: 在本次项目中,陈瑀涵承担了主仓库维护、Pull Request 合并,以及详情、状态管理、图片处理和多轮 UI 调整等工作。
她在页面细节和功能完整性方面投入了较多精力,能够在已有版本基础上持续发现问题并进行完善。
项目开发过程中,也推动了 README、测试记录和代码管理文档的整理,使最终成果不仅包含网页,还包含相对完整的使用和测试说明。

需要改进的地方: 后续可以进一步加强开发计划的阶段性安排,避免部分 UI 细节集中在开发后期反复修改。
同时,在新增较复杂的功能前,可以先明确数据结构、异常场景和测试方法,减少功能完成后的补充调整。

十一、个人总结与反思

这次结对编程让我更加熟悉了从原型设计到网页实现的过程。在搜索、发布和首页功能开发中,我体会到需要先理解用户真正要完成的操作,再组织页面元素和事件逻辑。例如,搜索功能不只是输入关键词,还要考虑类型、分类、地点和状态等条件如何组合;发布功能不只是展示表单,还需要处理错误输入以及发布后的页面反馈。通过多轮页面调整,我也认识到功能实现和用户体验需要同时考虑。一个功能即使能正常执行,如果入口不明显、提示不清楚或者页面布局不合理,也会影响最终使用效果。在 GitHub 协作方面,我学习了 Fork、功能分支、Commit、Push 和 Pull Request 的实际操作,也理解了每轮开发前同步最新主分支的重要性。
这次项目使我更加认识到结对开发需要合理分工、及时沟通和共同检查,而不是简单地把任务分成两半。今后如果继续进行类似项目,我希望能够更早确定功能验收标准,在开发过程中同步完成测试和文档整理,减少最后阶段的集中收尾工作。

附:项目相关链接

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:23  阿合加依娜尔  阅读(14)  评论(0)    收藏  举报