软件工程第二次结对作业

2026 秋软件工程第二次结对作业——校园失物招领 APP 程序实现

项目 内容
这个作业属于哪个课程 202601 软件工程
这个作业要求在哪里 2026 秋软件工程结对作业(第二次之程序实现)
这个作业的目标 基于第一次结对作业的 Figma 原型,完成校园失物招领核心模块的代码实现,使“发布信息—浏览或搜索—查看详情—联系发布者—更新状态”的流程清晰、可用
结对成员 102401409 陈宝荣;102401411 陈博淇
GitHub 仓库 bookiii98/102401409-102401411
Android APK 仓库中的 apk/campus-lost-found.apk
第一次结对作业博客 软件工程第一次结对作业之需求分析和原型设计
Figma 原型 点击查看校园失物招领小程序原型
陈宝荣本次作业博客 2026 秋软件工程第二次结对作业——校园失物招领 APP 程序实现
陈博淇本次作业博客 2026 秋软件工程第二次结对作业——校园失物招领 APP 程序实现

一、项目概述

校园中的失物招领信息经常分散在班级群、宿舍群和朋友圈中。消息更新速度快,失主与拾取者未必在同一信息渠道内;即使双方后来取得联系,原消息也常常没有及时标注处理结果,仍会产生重复询问。

本项目在第一次结对作业的需求分析和 Figma 原型基础上,将原型落实为可安装的 Android APP。我们将范围控制在作业要求的核心流程内,不实现复杂后台、实名认证、即时聊天和地图定位,优先保证以下闭环真实可用:

发布寻物/招领信息
        ↓
集中浏览或按关键词、类别检索
        ↓
查看物品详情与发布者联系方式
        ↓
物品找回或归还后由发布者更新状态

项目使用 HTML、CSS 和 JavaScript 完成页面与业务逻辑,以 LocalStorage 保存本地数据,再通过 Capacitor 封装为 Android APP。最终 APK 已提交到仓库,测试人员可以直接下载并安装。

1.1 已实现的核心功能

  • 发布寻物信息和招领信息;
  • 首页集中浏览当前有效信息;
  • 根据物品名称、描述、地点等关键词搜索;
  • 按物品类别筛选;
  • 查看物品图片、类型、名称、时间、地点、描述和发布者;
  • 查看并复制发布者提供的联系方式;
  • 在“我的发布”中将寻物信息标为“已找到”、将招领信息标为“已归还”;
  • 已解决信息不再出现在首页和发现页,减少无效联系;
  • 查看、编辑、删除自己发布的信息,并可将已解决信息恢复为进行中状态;
  • 使用 LocalStorage 持久化保存本地帖子。

1.2 在核心要求之外实现的附加功能

  • 物品类别筛选;
  • 发布前匹配相反类型、相同类别且标题相似的有效帖子;
  • 上传物品图片并即时预览,未上传图片时,按类别展示默认封面;
  • 针对 Android WebView 环境优化剪贴板兼容机制,并自动剥离多余前缀,仅复制纯净联系方式以提升粘贴搜索体验;
  • 智能识别详情页访问者身份,若是发布者本人则自动置灰并屏蔽获取联系方式操作;
  • 彻底修复移动端窄屏场景下的自适应 Flex 布局挤压变形问题;
  • 针对手机使用场景完成页面布局,并封装为 Android APK。

首页整体效果

【截图 1:首页整体效果】

发现页的关键词搜索与类别筛选

【截图 2:发现页的关键词搜索与类别筛选】

发布页 图片上传预览

【截图 3:发布页与图片上传预览】

详情页 安全提示 联系方式复制

【截图 4:详情页、安全提示和联系方式复制】

我的发布 状态修改 编辑 删除

【截图 5:我的发布、状态修改、编辑与删除】

二、结对分工与协作过程

2.1 具体分工

以下分工根据仓库中两位贡献者的实际提交内容整理:

成员 主要工作
陈博淇(102401411,GitHub:bookiii98 / bokii) 创建仓库与项目结构;设计初始数据和 LocalStorage 存储;完成首页数据浏览;合并 PR;合并后完成数据持久化、联系方式保存与复制、状态文案、编辑/删除/恢复、类别统一、时间和相似匹配修复、默认封面;使用 Capacitor 封装 Android APP;抽取可测试业务逻辑、补充 12 个自动化测试;完善 README
陈宝荣(102401409,GitHub:Calmigrhty) 完成首页 UI 结构与样式;补充测试图片;完成发现页及横向分类标签、发布表单与相似信息弹窗、我的发布与状态切换、详情页与隐私提示;修复 Android WebView 下的剪贴板降级复制 Bug 及移动端布局挤压问题;实现详情页自我视角下的交互屏蔽与纯净文本复制提取;通过分支提交并发起 PR #1 和 PR #2
两人共同完成 延续第一次作业的需求与原型;讨论功能范围和数据结构;联调核心业务流程;合并后回归检查;根据问题继续修复功能;整理交付材料

2.2 GitHub 协作证据

仓库历史中可以核实:

  1. 主仓库名称为 102401409-102401411,符合“两位学号组成仓库名”的要求;
  2. 两位成员都留下了独立提交记录;
  3. 陈宝荣使用 Calmigrhty/main 提交功能,陈博淇通过 Pull Request #1 合并到 bookiii98/main;
  4. 合并提交为 1799521 Merge pull request #1 from Calmigrhty/main;
  5. 合并后继续按功能拆分提交修复、扩展、APK 封装、测试与 README,而不是一次性集中签入。

PR #1 页面

【截图 6_1:PR #1 页面,截图中应显示发起人、分支和已合并状态】

PR #2页面

** 【截图6_2:PR #2页面,移动端布局与 WebView 剪贴板问题修复】**

三、PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 40
Estimate 估计这个任务需要多少时间 30 40
Development 开发 840 1090
Analysis 需求分析(包括学习新技术) 70 90
Design Spec 生成设计文档 50 45
Design Review 设计复审 30 40
Coding Standard 代码规范(为目前的开发制定合适的规范) 20 25
Design 具体设计 90 120
Coding 具体编码 360 470
Code Review 代码复审 70 90
Test 测试(自我测试、修改代码、提交修改) 150 210
Reporting 报告 140 170
Test Report 测试报告 60 70
Size Measurement 计算工作量 20 20
Postmortem & Process Improvement Plan 事后总结并提出过程改进计划 60 80
合计 1010 1300

3.1 PSP 偏差分析

实际耗时比预估多了约 290 分钟,主要超在编码和测试。原本以为第一次作业已经完成原型,实现时直接照着做就行,但实际还要处理状态切换、搜索筛选、图片上传、本地存储等细节。后面又增加了 Android 封装和模拟器调试,也花了不少时间。

测试阶段除了手动检查功能,还补了 12 个单元测试,并调整了部分代码结构。下次再做类似项目时,会提前给移动端适配、环境配置和测试留出更多时间。

四、解题思路与设计实现

4.1 技术选型

技术 用途 选择原因
HTML 组织首页、发现、发布、详情和个人页面 页面数量有限,原型结构明确,原生页面便于直接控制
CSS 实现卡片、标签、弹窗、底部导航和移动端布局 与 Figma 原型对应,避免引入不必要的 UI 框架
JavaScript 处理筛选、发布、详情、状态管理、编辑删除等业务 项目功能可以由前端完成,也便于抽取纯函数测试
LocalStorage 保存帖子数据 不依赖服务器即可保留本机数据,适合作业验收和离线演示
Node.js node:test + node:assert 自动化单元测试 Node.js 内置,无须额外测试依赖,运行命令简单
Capacitor 将 Web 资源封装为 Android APP 可以复用已完成的 Web 页面和业务逻辑,并交付 APK

当前版本没有后端服务器和云数据库。数据仅保存在安装 APP 的本地设备中,不同设备之间不会自动同步。这是本次作业范围内的技术取舍,不代表生产环境的最终方案。

4.2 页面与模块划分

  • index.html:首页,展示当前有效的寻物和招领信息;
  • discover.html:发现页,负责关键词搜索和类别筛选;
  • publish.html:发布/编辑页面,负责表单校验、图片上传和相似信息提示;
  • detail.html:详情页,负责展示完整信息、安全提示和联系方式复制;
  • profile.html:我的发布,负责状态更新、恢复、编辑和删除;
  • storage.js:LocalStorage 的初始化、查询、保存、更新和删除;
  • searchService.js:搜索、类别和有效状态的组合筛选;
  • validation.js:发布信息必填项校验;
  • postService.js:相似信息匹配与状态文字转换;
  • media.js:图片与类别默认封面处理。

4.3 核心业务流程图

flowchart TD A[进入 APP] --> B[首页浏览当前有效信息] B --> C{是否需要检索} C -->|是| D[进入发现页] D --> E[输入关键词或选择类别] E --> F[查看筛选结果] C -->|否| G[直接选择信息卡片] F --> G G --> H[查看物品详情] H --> I{信息是否仍有效} I -->|否| J[显示已找到或已归还] I -->|是| K[阅读安全提示] K --> L[查看并复制联系方式] L --> M[线下联系发布者] M --> N[发布者进入我的发布] N --> O[标记为已找到或已归还] O --> P[该信息退出公开有效列表]

4.4 发布与相似信息匹配流程图

flowchart TD A[进入发布页] --> B[选择寻物或招领] B --> C[填写名称、类别、地点、描述和联系方式] C --> D[可选:上传图片] D --> E[提交表单] E --> F{标题和联系方式是否合法} F -->|否| G[显示明确提示并停止提交] G --> C F -->|是| H[查找相反类型、同类别、标题相似的有效帖子] H --> I{是否存在相似信息} I -->|是| J[显示可能匹配的信息] J --> K{用户是否先核对} K -->|是| L[跳转相似信息详情] K -->|否| M[继续发布] I -->|否| M M --> N[写入 LocalStorage] N --> O[显示发布成功] O --> P[返回首页或查看我的发布]

4.5 数据流说明

flowchart LR U[用户操作] --> P[页面交互层] P --> V[校验 validation.js] P --> Q[搜索 searchService.js] P --> S[业务规则 postService.js] V --> T[storage.js] Q --> T S --> T T <--> L[(LocalStorage: lostFoundPosts)] T --> R[首页/发现/详情/我的发布重新渲染]

数据读写统一通过 storage.js 完成。页面不另外维护一份永久数据,从而避免首页、详情页和“我的发布”之间出现状态不一致。帖子主要字段如下:

{
  id: "post-001",
  type: "found",          // lost: 寻物;found: 招领
  category: "数码电子",
  title: "AirPods Max银色",
  location: "主图书馆四楼自习室",
  description: "物品详细描述",
  contactType: "微信",
  contact: "student_a",
  publisher: "陈宝荣",
  publishTime: "2026-10-07 13:45",
  status: "active",       // active 或 resolved
  image: "assets/images/airpods.jpg"
}

五、关键代码实现与说明

5.1 组合搜索与自动过滤已解决信息

发现页不只搜索标题,也会匹配描述、地点和类别;类别条件与关键词条件是“与”的关系。同时,resolved 信息不会再出现在公开搜索结果中。

function filterPosts(posts, category = "all", searchQuery = "") {
  const safePosts = Array.isArray(posts) ? posts : [];
  const keyword = String(searchQuery || "").trim().toLowerCase();

  return safePosts.filter(post => {
    const isActive = post.status === "active";
    const matchCategory =
      category === "all" || post.category === category;

    const searchableFields = [
      post.title,
      post.description,
      post.location,
      post.category
    ];

    const matchSearch =
      keyword === "" ||
      searchableFields.some(value =>
        String(value || "").toLowerCase().includes(keyword)
      );

    return isActive && matchCategory && matchSearch;
  });
}

这样处理后,空关键词表示不限制搜索,字段缺失也不会使程序报错;物品解决后会自动退出公开列表,但仍保留在发布者的历史记录中。

5.2 LocalStorage 统一数据读写

const STORAGE_KEY = "lostFoundPosts";

function getPosts() {
  initPosts();
  const posts = localStorage.getItem(STORAGE_KEY);
  return JSON.parse(posts);
}

function savePosts(posts) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(posts));
}

function getPostById(id) {
  const posts = getPosts();
  return posts.find(post => post.id === id);
}

function deletePost(id) {
  const posts = getPosts();
  const remainingPosts = posts.filter(post => post.id !== id);

  if (remainingPosts.length === posts.length) {
    return false;
  }

  savePosts(remainingPosts);
  return true;
}

storage.js 将数据初始化、读取、保存、按 ID 查询、更新和删除放到同一层。页面只调用这些接口,既减少重复代码,也让跨页面数据状态保持一致。

5.3 发布前相似信息匹配

function findSimilarPost(posts, type, category, title) {
  const safePosts = Array.isArray(posts) ? posts : [];
  const normalizedTitle = String(title || "").trim();
  const targetType = type === "lost" ? "found" : "lost";

  return safePosts.find(post =>
    post.status === "active" &&
    post.type === targetType &&
    post.category === category &&
    (
      String(post.title || "").includes(normalizedTitle) ||
      normalizedTitle.includes(String(post.title || ""))
    )
  ) || null;
}

发布“寻物”时只检查“招领”,发布“招领”时只检查“寻物”;同时要求类别相同、状态有效、标题互相包含。它不是复杂的推荐算法,但可以在不引入后端和搜索引擎的前提下,先提醒用户核对可能已经存在的对应信息。

5.4 状态闭环

function togglePostStatus(id) {
  const posts = getPosts();
  const targetPost = posts.find(p => p.id === id);
  if (!targetPost) return;

  const isResolved = targetPost.status === "resolved";
  targetPost.status = isResolved ? "active" : "resolved";
  savePosts(posts);
  renderMyPosts();
}

function getResolvedStatusText(type) {
  return type === "lost" ? "已找到" : "已归还";
}

用户只能从“我的发布”管理自己的信息。状态从 active 切换到 resolved 后,寻物信息显示“已找到”,招领信息显示“已归还”;公开列表只展示 active 数据。程序还允许用户在确认后恢复状态,处理误操作。

5.5 联系方式复制的兼容处理

在将 Web 原型打包为 Android APK 后,WebView 环境的安全机制对剪贴板操作提出了更高要求。我们在 detail.js 中重构了复制逻辑,不仅保留了对现代 API 的调用,还深入解决了原生套壳应用中的异常捕获跳跃与选中失焦问题。

const fallbackCopy = () => {
  const temporaryInput = document.createElement('textarea');
  temporaryInput.value = text;
  temporaryInput.setAttribute('readonly', '');
  temporaryInput.style.position = 'fixed';
  temporaryInput.style.opacity = '0';
  document.body.appendChild(temporaryInput);
  
  // 核心修复1:移动端WebView强制要求隐藏元素先 focus 聚焦,否则无法 select
  temporaryInput.focus();
  temporaryInput.select();
  // 核心修复2:针对部分机型 select 失效,坐标强行全选
  temporaryInput.setSelectionRange(0, 99999); 
  
  const copied = document.execCommand('copy');
  temporaryInput.remove();
  if (!copied) throw new Error('降级复制方案也失败了');
};

try {
  if (navigator.clipboard && window.isSecureContext) {
    try {
      await navigator.clipboard.writeText(text); // 尝试现代 API
    } catch (err) {
      // 核心修复3:安卓权限拦截不会走else,而是抛出异常,需在catch中强行调用降级方案
      fallbackCopy();
    }
  } else {
    fallbackCopy();
  }
} catch (error) { ... }

上述代码确保了无论是在电脑浏览器,还是在严格限制系统权限的安卓手机中,用户都能无缝完成一键复制。

六、附加特点设计与展示

6.1 类别筛选

设计意义:校园失物信息数量增加后,只使用关键词可能难以准确描述物品。类别筛选可以先缩小范围,也能与关键词组合使用。

实现思路:页面将所选类别传给 filterPosts();当类别为 all 时不限制,否则只保留 post.category === category 的数据。

实现成果:支持“全部、数码电子、日常用品、卡片证件、交通工具、其他”等类别,并确保发布页与发现页使用一致的类别名称。

选择某一类别后的结果1 选择某一类别后的结果2

【截图 7:选择某一类别后的结果】

6.2 一键复制联系方式与安全提示

设计意义:联系方式是用户真正完成线下联系的关键,但不应在进入详情页时直接突出展示。先给出安全提示,再展示并复制联系方式,可以兼顾效率和隐私提醒。并在复制操作时最大程度减少用户的“冗余动作”;此外,发布者自己进入详情页时,不应看到多余的联系引导。

实现思路:点击“获取联系方式”后先打开安全提示弹窗;确认后显示联系方式,并通过 Clipboard API 或兼容方案复制;代码在抓取文本时自动抛弃了“微信:”等前缀信息,仅将纯净账号字符存入剪贴板,方便用户直接跳到微信或电话中粘贴并搜索;在 bindEvents 中加入了对 post.publisher === "我自己" 的识别,如果是用户本人,按钮直接置灰并提示“这是您发布的帖子”,彻底屏蔽弹窗与复制逻辑。

实现成果:用户可以一键复制“联系方式类型 + 联系方式”,复制成功后按钮短暂显示“已复制”;已解决信息不再提供联系入口。

安全提示 联系方式展示 复制成功状态

【截图 8:安全提示、联系方式展示和复制成功状态】

6.3 相似失物招领匹配

设计意义:如果失主发布寻物时,平台中已经存在可能对应的招领信息,直接提示核对比继续增加重复信息更有效。

实现思路:发布前在 LocalStorage 中寻找“相反类型 + 相同类别 + 有效状态 + 标题相似”的第一条记录;找到后显示匹配弹窗,用户可前往详情或继续发布。

实现成果:相似提醒不会阻止用户发布,既提供线索,也避免简单匹配造成误判后强行拦截。

相似信息匹配弹窗和跳转结果1 相似信息匹配弹窗和跳转结果2

【截图 9:相似信息匹配弹窗和跳转结果】

6.4 图片上传与类别默认封面

设计意义:图片可以显著提高物品辨识度;但发布者未必都有照片,因此不能让缺图导致卡片空白或布局破坏。

实现思路:使用 FileReader 将用户选择的图片转换为 Base64 字符串后存入 LocalStorage;没有图片或图片加载失败时,根据物品类别显示对应默认封面。

实现成果:用户选择图片后可以立即预览;无图片帖子仍保持完整、统一的卡片展示效果。

上传图片预览 不同类别默认封面对比

【截图 10:上传图片预览与不同类别默认封面对比】

6.5 发布内容的完整管理

除了作业要求的状态更新,我们还实现了编辑、删除和恢复状态。编辑时保留原帖 ID、发布者、发布时间和当前状态;删除前要求用户确认;恢复状态时也显示与类型相符的确认信息。

编辑 删除确认 恢复状态

【截图 11:编辑、删除确认和恢复状态】

七、项目目录与使用说明

7.1 目录组织

102401409-102401411/
├── README.md                       # 项目说明、目录和运行方式
├── package.json                    # 依赖与 npm test 命令
├── package-lock.json
├── capacitor.config.json           # Capacitor 配置
│
├── apk/
│   └── campus-lost-found.apk       # 最终可安装 Android APK
│
├── android/                        # Capacitor 生成的 Android 原生工程
│
├── tests/
│   ├── businessLogic.test.js       # 校验、相似匹配、状态文案测试
│   └── searchService.test.js       # 搜索与筛选测试
│
└── www/                            # APP 使用的 Web 资源
    ├── index.html                  # 首页
    ├── discover.html               # 发现/搜索页
    ├── publish.html                # 发布/编辑页
    ├── detail.html                 # 详情页
    ├── profile.html                # 我的发布
    │
    ├── css/
    │   ├── common.css
    │   ├── home.css
    │   ├── discover.css
    │   ├── publish.css
    │   ├── detail.css
    │   └── profile.css
    │
    ├── js/
    │   ├── data.js                 # 初始示例数据
    │   ├── storage.js              # LocalStorage 数据操作
    │   ├── media.js                # 图片和默认封面
    │   ├── home.js
    │   ├── discover.js
    │   ├── publish.js
    │   ├── detail.js
    │   ├── profile.js
    │   ├── searchService.js        # 搜索/筛选纯函数
    │   ├── validation.js           # 发布校验纯函数
    │   └── postService.js          # 匹配和状态纯函数
    │
    └── assets/images/              # 示例图片与类别默认封面

HTML、CSS、JavaScript、测试、Android 工程和最终 APK 分目录保存。浏览器直接使用的源文件集中在 www/;可独立测试的业务规则位于 www/js/;自动化测试位于 tests/;最终交付物放在 apk/,便于评分人员快速定位。

7.2 测试人员运行 APP

  1. 打开 GitHub 仓库;
  2. 下载 apk/campus-lost-found.apk;
  3. 将 APK 传到 Android 手机并安装;
  4. 如果系统阻止安装未知来源应用,请只对当前使用的浏览器或文件管理器允许安装,再重新安装;
  5. 打开“校园失物招领”APP,通过底部导航使用首页、发现、发布和我的四个模块。

APK 仅适用于 Android 设备,不能直接安装到 iPhone/iPad。当前仓库中的 APK 文件约为 12 MB。

7.3 从 Android 工程运行

环境要求:Node.js、npm、Android Studio 和可用的 Android SDK。

npm install
npx cap sync android
npx cap open android

随后在 Android Studio 中选择 Android 模拟器或真机运行。

7.4 浏览器预览

核心页面位于 www/。可以使用 Chrome 打开 www/index.html 进行基础预览;最终验收建议以仓库中的 APK 为准。

7.5 运行单元测试

npm install
npm test

预期结果:

tests 12
pass 12
fail 0

八、单元测试

8.1 测试工具的选择与学习过程

项目使用 Node.js 内置的 node:test 测试运行器和 node:assert/strict 断言库。选择它们的原因是:

  • 与项目使用的原生 JavaScript 技术路线一致;
  • Node.js 自带,无须再引入 Jest、Mocha 等依赖;
  • 可以直接通过 npm test 自动发现并运行 tests/*.test.js;
  • 失败时会显示用例名称、断言差异和出错位置,适合持续回归测试。

我们的实现顺序是先将与 DOM 无关的规则抽取为纯函数,再分别测试搜索筛选、发布校验、相似匹配和状态文字。这样既能让浏览器继续直接使用这些函数,也能通过 module.exports 在 Node.js 中加载它们。

8.2 一份简易的 node:test 教程

  1. 安装 Node.js;
  2. 在项目中建立 tests/ 目录,并以 .test.js 作为测试文件后缀;
  3. 引入测试运行器和严格断言:
const test = require("node:test");
const assert = require("node:assert/strict");
  1. 引入需要测试的函数;
  2. 使用 test("用例说明", () => { ... }) 定义用例;
  3. 使用 assert.equal()、assert.ok() 等判断实际结果是否符合预期;
  4. 在 package.json 中配置命令:
{
  "scripts": {
    "test": "node --test tests/*.test.js"
  }
}
  1. 执行 npm test,看到 fail 0 表示全部通过。

8.3 测试对象与 12 个用例

编号 被测函数 测试内容 预期结果 方法
T01 filterPosts 根据标题关键词 AirPods 搜索 只返回目标帖子 等价类
T02 filterPosts 关键词仅出现在描述中 仍能搜索到帖子 等价类
T03 filterPosts 搜索完全不存在的关键词 返回空数组 异常/空结果
T04 filterPosts 按“卡片证件”筛选 只返回对应类别 等价类
T05 filterPosts 相同类别中包含 resolved 帖子 只返回 active 帖子 状态分支
T06 filterPosts 类别与关键词同时生效 返回两个条件的交集 组合条件
T07 validatePostInput 标题为空、联系方式合法 校验失败并提示填写名称 白盒分支
T08 validatePostInput 标题合法、联系方式为空 校验失败并提示填写联系方式 白盒分支
T09 validatePostInput 标题与联系方式均合法 校验成功 白盒分支
T10 findSimilarPost 相反类型、同类别、标题相似、状态有效 找到对应帖子 组合条件
T11 findSimilarPost 相似帖子已经解决 返回 null 状态分支
T12 getResolvedStatusText 分别输入 lost 和 found 返回“已找到”和“已归还” 分支测试

8.4 部分测试代码与被测函数

以下代码测试 validatePostInput() 的三条执行路径:

test("T07:标题为空时发布校验失败", () => {
  const result = validatePostInput("", "wx123");
  assert.equal(result.valid, false);
  assert.equal(result.message, "请填写物品名称");
});

test("T08:联系方式为空时发布校验失败", () => {
  const result = validatePostInput("黑色雨伞", "");
  assert.equal(result.valid, false);
  assert.equal(result.message, "请填写联系方式");
});

test("T09:标题和联系方式均合法时校验成功", () => {
  const result = validatePostInput("黑色雨伞", "wx123");
  assert.equal(result.valid, true);
  assert.equal(result.message, "");
});

以下代码测试相似信息匹配函数是否会排除已经解决的信息:

test("T11:已解决帖子不参与相似信息匹配", () => {
  const posts = [{
    id: "1",
    type: "found",
    category: "数码电子",
    title: "AirPods Max银色",
    status: "resolved"
  }];

  const result = findSimilarPost(
    posts,
    "lost",
    "数码电子",
    "AirPods Max银色"
  );

  assert.equal(result, null);
});

8.5 白盒测试:T07~T09 分支覆盖

validatePostInput() 的控制流如下:

flowchart TD A[开始] --> B{标题去除空格后是否为空} B -->|是| C[返回 valid=false:请填写物品名称] B -->|否| D{联系方式去除空格后是否为空} D -->|是| E[返回 valid=false:请填写联系方式] D -->|否| F[返回 valid=true]

该函数有两个判断条件和三个返回出口:

  • T07 使第一个判断为真,覆盖“标题为空”的失败分支;
  • T08 使第一个判断为假、第二个判断为真,覆盖“联系方式为空”的失败分支;
  • T09 使两个判断都为假,覆盖合法输入的成功分支。

因此,T07~T09 覆盖了该函数所有语句、所有判断的真/假结果以及三个返回出口,达到了本函数的分支覆盖目标。

8.6 测试数据构造与“测试人员的刁难”

测试数据按以下思路设计:

  • 正常数据:合法标题、联系方式和完整帖子,确认正常路径可用;
  • 缺失数据:分别缺少标题和联系方式,确认程序给出明确提示而不是继续保存;
  • 搜索边界:关键词出现在标题或描述、关键词不存在、空结果;
  • 组合条件:同时使用类别和关键词,确认结果取交集而不是并集;
  • 状态条件:将帖子设为 resolved,确认其不再出现在公开结果和匹配结果中;
  • 业务方向:相似信息必须是相反类型,例如寻物只匹配招领;
  • 显示语义:寻物解决后显示“已找到”,招领解决后显示“已归还”。

如果测试人员故意输入空标题、空联系方式、不存在的关键词,或用已解决信息参与搜索/匹配,程序都应返回稳定、可解释的结果,而不是报错。对于页面层,还应人工检查重复点击状态按钮、删除不存在的信息、图片缺失和剪贴板不可用等情况。

8.7 自动化测试结果

在仓库当前版本执行 npm test,12 个用例全部通过:

tests 12
pass 12
fail 0
cancelled 0
skipped 0
todo 0

B473119F426F4A7C17692B618A9A35B4

【截图 12:终端运行 npm test,完整显示 tests 12 / pass 12 / fail 0】

8.8 对测试充分性的评价

这 12 个用例达到了作业“至少 10 个测试用例、采用白盒测试方法并实现自动化”的要求,也覆盖了本项目最重要的纯业务规则:搜索、筛选、校验、相似匹配和状态文字。

但它们并不等于整个 APP 已被完全测试。目前尚未自动覆盖 LocalStorage 的增删改查、图片读取、DOM 事件、页面跳转、剪贴板兼容和 Android WebView 中的完整交互。因此,本次采用“自动化单元测试 + Android 真机人工走查”结合的方式。后续可以继续为存储层注入模拟 LocalStorage,并增加端到端测试。

九、GitHub 代码签入记录

项目按“初始化—数据层—页面功能—PR 合并—联调修复—Android 封装—测试—文档”的顺序推进。当前仓库可核实的完整提交记录如下:

日期 Commit 作者 内容
2026-10-07 32fce91 陈博淇 chore: initialize project structure
2026-10-08 0406c24 陈博淇 feat: add initial lost-and-found post data
2026-10-08 3ef5106 陈博淇 feat: implement local post storage
2026-10-08 1cab455 陈博淇 feat: build home item browsing
2026-10-08 535c1ee 陈博淇 docs: add development conventions
2026-10-08 38e10f5 陈宝荣 feat: implement home page UI structure and styles
2026-10-08 5c7f3cd 陈宝荣 chore: add sample images for testing
2026-10-08 618e852 陈宝荣 feat: build discover page with horizontal scroll tabs
2026-10-08 6302a03 陈宝荣 feat: implement publish form and duplicate check modal
2026-10-08 238d339 陈宝荣 feat: build profile page with user posts and status toggle
2026-10-08 0157b01 陈宝荣 fix: calculate resolved status count from local data
2026-10-09 b729e96 陈宝荣 feat: implement detail page with immersive header and privacy modal
2026-10-09 c0f6e3e 陈宝荣 fix: center success banner and fix layout overflow on detail page
2026-10-09 1799521 合并提交 Merge pull request #1 from Calmigrhty/main
2026-10-09 ab71a80 陈博淇 fix: persist profile posts and hide resolved items
2026-10-09 1a604f0 陈博淇 feat: save and copy post contact information
2026-10-09 45001f6 陈博淇 fix: allow closing contact information banner
2026-10-09 f1734cf 陈博淇 fix: show type-specific resolved status
2026-10-09 b3e6a39 陈博淇 fix: remove misleading demo content
2026-10-09 3d2bf7a 陈博淇 feat: add post edit and delete actions
2026-10-09 65c7254 陈博淇 feat: allow restoring resolved post status
2026-10-09 5251760 陈博淇 fix: align category options across pages
2026-10-09 0e5bc76 陈博淇 fix: correct time handling and active post matching
2026-10-09 25b6244 陈博淇 feat: add category default cover images
2026-10-10 be96bb4 陈博淇 feat: package app with Capacitor Android
2026-10-10 6522ede 陈博淇 refactor: extract business logic and add unit tests
2026-10-10 bf07a25 陈博淇 docs: update project README
2026-10-10 6eb8ce8 陈宝荣 fix: resolve mobile layout overflow and webview clipboard copy issues
2026-10-10 033d6d2 陈博淇 Merge pull request #2 from Calmigrhty/main

提交信息使用 feat、fix、docs、refactor、chore 等前缀区分功能、修复、文档、重构和维护工作。记录中既有两位成员的功能提交,也有 PR 合并后的独立修复和测试提交,可以对应到实际功能演进过程。

3B834FC62FDA1ACC144D8F9CC9AC1501

A6E106F70E28A8859410B6C928292F45

D9262469E61277B9C1FFF19FF6AEC0EF

【截图 13:GitHub Commits 页面,确保能看清两位贡献者和主要提交】

十、遇到的问题、尝试与解决方法

10.1 页面统计与真实数据不一致

问题描述:“我的发布”顶部已解决数量如果使用固定展示值,发布或修改状态后就会与实际列表不一致。

做过的尝试:先完成页面布局和示例展示,再在联调时对照 LocalStorage 中的真实帖子检查统计结果。

解决方法:从 myPosts 中动态筛选 status === "resolved" 的帖子并计算数量,对应修复提交为 0157b01。

是否解决:已解决。状态变化后重新渲染页面,统计值随数据更新。

收获:原型中的展示数字在真实程序中必须由数据计算,不能继续保留为静态示例。

10.2 合并后页面之间的数据语义需要统一

问题描述:分工开发可以提高速度,但页面合并后需要统一类别名称、联系方式字段、状态文案、时间格式和“有效信息”的判断;否则同一条数据可能在不同页面呈现不一致。

做过的尝试:完成 PR 合并后,从“发布—首页/发现—详情—我的发布—状态修改”重新走查。仓库后续提交依次修复了持久化、联系方式、状态文案、类别、时间和有效帖子匹配等问题。

解决方法:统一帖子字段和类别选项;用 active/resolved 表示状态;用 getResolvedStatusText() 区分“已找到”和“已归还”;所有页面统一从 storage.js 获取数据。

是否解决:当前核心流程已经统一,相关修复记录为 ab71a80、1a604f0、f1734cf、5251760 和 0e5bc76。

收获:结对开发不能只保证各自页面能运行,还要尽早约定共享字段和状态含义,并在合并后进行完整业务回归。

10.3 浏览器页面逻辑难以直接做单元测试

问题描述:早期搜索、校验和状态判断混在 DOM 事件中,如果直接加载整个页面进行测试,会依赖浏览器环境,测试不稳定且难以定位失败原因。

做过的尝试:先识别出不依赖页面的规则,再把它们从页面脚本中抽离。

解决方法:建立 searchService.js、validation.js 和 postService.js,使用“浏览器直接调用 + Node.js module.exports”的方式复用同一套逻辑,随后编写 12 个自动化用例。

是否解决:已解决,当前运行 npm test 为 12/12 通过,对应提交 6522ede。

收获:可测试性不是编码结束后才补的属性。将业务规则写成输入明确、输出稳定的纯函数,既便于复用,也便于覆盖分支和回归。

10.4 从 Web 原型到 Android APP 的交付

问题描述:本次选择 APP 呈现形式,最终不仅要在浏览器中显示页面,还必须提供可安装并能展示预期效果的 APK。

做过的尝试:保留 www/ 作为统一 Web 资源目录,在功能稳定后再接入 Capacitor,避免页面功能与 Android 工程同时频繁变化。

解决方法:添加 Capacitor 配置与 Android 工程,将 www/ 同步到原生工程,并生成 apk/campus-lost-found.apk。联系方式复制还提供了兼容方案,以适应不同上下文。

是否解决:仓库已包含约 12 MB 的最终 APK 和完整 Android 工程。

收获:选择 APP 作为呈现形式时,应尽早明确 Web 资源目录和打包流程,但把最终封装安排在核心功能稳定之后更容易定位问题。

10.5 Web前端打包 Android APK 的移动端兼容问题修复

问题描述: 在通过 Capacitor 封装为 Android APK 并装入真机测试时,出现了两个明显的 Bug:一是底部导航栏按钮被严重横向挤压变形;二是点击“复制联系方式”总是提示失败。

做过的尝试:

  • 对于布局:原以为是全面屏底部小白条遮挡,加了安全区 padding 后发现依然被横向拉伸。
  • 对于复制功能:代码中已经预留了 execCommand 作为旧版浏览器的 else 降级分支,但在手机真机上依旧失效。

解决方法:

  • 布局重构:将底部导航栏的 gap 固定像素间距,改为使用 Flex 布局的 justify-content: space-evenly,并缩小左右 padding,让四个按钮自动根据不同手机的物理屏幕宽度等比均分。

  • 异常捕获接管降级:通过分析 Android WebView 机制发现,手机浏览器是支持 navigator.clipboard 的(通过了 if 判断),但在执行写入时因缺乏原生操作系统的剪贴板授权,导致抛出了 Error。由于抛出异常越过了 else 分支直接进入全局 catch,导致降级方案根本没执行。我们把降级方案提取成函数,在内层 catch 中拦截系统权限报错并强制唤起降级。

  • WebView 失焦适配:在隐藏文本框 appendChild 后,增加 focus() 强制获取焦点与 setSelectionRange(0, 99999),解决移动端对非可视区域元素无法执行 select() 拷贝的限制。

是否解决: 已完美解决。更新代码、执行 npx cap sync android 并利用 Gradle 重新编译后,新 APK 的排版等比分布,点击复制不仅顺利执行,而且成功写入了纯净账号。

收获: 原生套壳应用(WebView)与纯现代浏览器在底层安全机制上差异巨大;不能完全迷信 if-else 的特性检测,针对现代 API 的 try-catch 异常捕获机制同样是触发降级方案的关键防线。

十一、队友评价

11.1 陈宝荣对陈博淇的评价

值得学习的地方:

这次合作中,陈博淇主要负责数据存储、业务逻辑和后期收尾。像状态切换、联系方式保存、编辑删除这些功能出现问题时,都会继续排查和修改,后面还完成了 Android 封装、单元测试和 README 整理。比较值得学习的是做事情比较完整,不会只做到“能运行”,而是会继续把细节补齐。

需要改进的地方:

有些内容是在开发后期才补上的,比如单元测试和 Android 端适配,所以后面出现了一些返工。如果一开始就把测试、移动端运行和最终提交形式一起考虑进去,整个开发过程会更顺一些。

11.2 陈博淇对陈宝荣的评价

值得学习的地方:

陈宝荣这次主要负责页面和交互实现,首页、发现页、发布页、个人页和详情页的主体结构都是他完成的,页面完成度比较高。后面 Android 端测试时发现移动端布局溢出和剪贴板复制的问题,他也继续进行了修改,并通过 PR 提交修复,没有只停留在最初版本。

需要改进的地方:

Git 提交整体比较清楚,但 PR 的说明有时写得比较简单,比如第一次 PR 的标题和描述都比较概括。以后如果能把每次修改的内容、原因和影响写得更具体一些,代码复审和后续回看都会更方便。

十二、总结与后续改进

这次作业完成了从需求与 Figma 原型到可安装 Android APP 的转换。最终程序围绕“发布—浏览/搜索—详情—联系—状态更新”形成完整闭环,并在此基础上加入类别筛选、联系方式复制、相似信息匹配、图片与默认封面、编辑删除和状态恢复等功能。

在软件工程过程方面,我们使用按功能拆分的 Git 提交和 Pull Request 完成结对协作,将可测试业务规则从页面代码中抽离,编写了 12 个可重复执行的自动化测试,其中 T07~T09 对发布校验完成了分支覆盖。当前仓库同时提供源代码、测试、Android 工程、README 和最终 APK,测试人员可以按说明复现。

项目仍有明确限制:LocalStorage 数据不能跨设备同步;相似匹配只使用类别和标题包含关系;自动化测试尚未覆盖 DOM、LocalStorage 和 Android 端到端交互。如果继续迭代,可以在不破坏当前核心流程的前提下,引入后端数据同步、更加稳健的相似度计算和完整的端到端测试。

十三、发布前检查清单

posted @ 2026-10-10 23:50  Calmigrhty  阅读(5)  评论(0)    收藏  举报