软件工程第二次结对作业
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 协作证据
仓库历史中可以核实:
- 主仓库名称为
102401409-102401411,符合“两位学号组成仓库名”的要求; - 两位成员都留下了独立提交记录;
- 陈宝荣使用
Calmigrhty/main提交功能,陈博淇通过 Pull Request #1 合并到bookiii98/main; - 合并提交为
1799521 Merge pull request #1 from Calmigrhty/main; - 合并后继续按功能拆分提交修复、扩展、APK 封装、测试与 README,而不是一次性集中签入。

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

** 【截图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 核心业务流程图
4.4 发布与相似信息匹配流程图
4.5 数据流说明
数据读写统一通过 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 的数据。
实现成果:支持“全部、数码电子、日常用品、卡片证件、交通工具、其他”等类别,并确保发布页与发现页使用一致的类别名称。

【截图 7:选择某一类别后的结果】
6.2 一键复制联系方式与安全提示
设计意义:联系方式是用户真正完成线下联系的关键,但不应在进入详情页时直接突出展示。先给出安全提示,再展示并复制联系方式,可以兼顾效率和隐私提醒。并在复制操作时最大程度减少用户的“冗余动作”;此外,发布者自己进入详情页时,不应看到多余的联系引导。
实现思路:点击“获取联系方式”后先打开安全提示弹窗;确认后显示联系方式,并通过 Clipboard API 或兼容方案复制;代码在抓取文本时自动抛弃了“微信:”等前缀信息,仅将纯净账号字符存入剪贴板,方便用户直接跳到微信或电话中粘贴并搜索;在 bindEvents 中加入了对 post.publisher === "我自己" 的识别,如果是用户本人,按钮直接置灰并提示“这是您发布的帖子”,彻底屏蔽弹窗与复制逻辑。
实现成果:用户可以一键复制“联系方式类型 + 联系方式”,复制成功后按钮短暂显示“已复制”;已解决信息不再提供联系入口。

【截图 8:安全提示、联系方式展示和复制成功状态】
6.3 相似失物招领匹配
设计意义:如果失主发布寻物时,平台中已经存在可能对应的招领信息,直接提示核对比继续增加重复信息更有效。
实现思路:发布前在 LocalStorage 中寻找“相反类型 + 相同类别 + 有效状态 + 标题相似”的第一条记录;找到后显示匹配弹窗,用户可前往详情或继续发布。
实现成果:相似提醒不会阻止用户发布,既提供线索,也避免简单匹配造成误判后强行拦截。

【截图 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
- 打开 GitHub 仓库;
- 下载
apk/campus-lost-found.apk; - 将 APK 传到 Android 手机并安装;
- 如果系统阻止安装未知来源应用,请只对当前使用的浏览器或文件管理器允许安装,再重新安装;
- 打开“校园失物招领”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 教程
- 安装 Node.js;
- 在项目中建立
tests/目录,并以.test.js作为测试文件后缀; - 引入测试运行器和严格断言:
const test = require("node:test");
const assert = require("node:assert/strict");
- 引入需要测试的函数;
- 使用
test("用例说明", () => { ... })定义用例; - 使用
assert.equal()、assert.ok()等判断实际结果是否符合预期; - 在
package.json中配置命令:
{
"scripts": {
"test": "node --test tests/*.test.js"
}
}
- 执行
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() 的控制流如下:
该函数有两个判断条件和三个返回出口:
- 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

【截图 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 合并后的独立修复和测试提交,可以对应到实际功能演进过程。



【截图 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 端到端交互。如果继续迭代,可以在不破坏当前核心流程的前提下,引入后端数据同步、更加稳健的相似度计算和完整的端到端测试。

浙公网安备 33010602011771号