2026秋软件工程结对作业(第二次之程序实现)
软件工程第二次结对作业——校园失物招领程序实现
| 项目 | 内容 |
|---|---|
| 课程 | 202601 软件工程 |
| 作业要求 | 第二次结对作业:程序实现 |
| 作业目标 | 基于上次原型,实现发布、浏览、搜索、详情、联系、本人编辑与状态更新的完整流程 |
| 结对成员 | 102400412 龚巍斌 / 102401111 郑瀚 |
| 结对同学博客 | 同伴博客 |
| 本文链接 | 失物招领程序实现 |
| GitHub 仓库 | illnessemc/102400412-102401111 |
| 上次作业 | 需求分析与原型设计 |
上次作业中,我们用墨刀设计了寻物、招领、搜索、详情和“我的发布”等页面。本次选择 Web 实现,将原型中的点击路径变成真正可保存、可查询、可更新的数据流程。项目以仓库根目录的 index.html 为正式入口;html/ 保存的是上次作业的原型页面。
一、具体分工
| 成员 | 主要工作 |
|---|---|
| 龚巍斌 | 首页浏览与类型、类别、地点筛选;关键词搜索;详情与联系方式;整合共享服务,补充本人编辑、表单缺项提示、审查修复和自动化测试。 |
| 郑瀚 | 寻物与招领发布、发布成功页;“我的发布”;状态维护与删除;对应页面的交互实现。 |
二、PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 40 | 50 |
| Estimate | 估计这个任务需要多少时间 | 40 | 50 |
| Development | 开发 | 1320 | 1630 |
| Analysis | 需求分析(包括学习新技术) | 120 | 140 |
| Design Spec | 生成设计文档 | 95 | 110 |
| Design Review | 设计复审 | 45 | 55 |
| Coding Standard | 代码规范(为目前的开发制定合适的规范) | 30 | 35 |
| Design | 具体设计 | 140 | 175 |
| Coding | 具体编码 | 500 | 660 |
| Code Review | 代码复审 | 110 | 140 |
| Test | 测试(自我测试、修改代码、提交修改) | 280 | 315 |
| Reporting | 报告 | 80 | 120 |
| Test Report | 测试报告 | 40 | 60 |
| Size Measurement | 计算工作量 | 15 | 20 |
| Postmortem & Process Improvement Plan | 事后总结并提出过程改进计划 | 25 | 40 |
| 合计 | 1440 | 1800 |
实际比预估多用 360 分钟,主要增加在具体编码、页面与共享数据的集成,以及测试后修改上。
三、解题思路与设计实现
1. 实现思路
① 先确定页面与使用路径。 对照上次的墨刀原型,我们将程序分为首页、搜索、详情、发布、发布成功、我的发布和编辑信息几个页面。用户从首页浏览或搜索,进入详情核对线索;发布者填写表单后查看结果,再到“我的发布”更正内容或结束信息。这条路径决定了页面之间需要传递记录编号、查询条件和返回位置。
② 再拆开页面交互与业务规则。 前端使用 HTML、CSS 和原生 JavaScript。js/app.js 负责路由与导航,home.js、search.js、detail.js 等处理页面展示;publish.js 提供发布与编辑共用的表单,edit.js 负责回填本人记录。查询、状态和数据格式集中在 posts.js,发布、编辑和权限检查集中在 manage.js。页面调用同一套规则,业务函数也能直接由 Node.js 单元测试验证。
③ 解决信息能否被别人看到。 直接用 Chrome 打开根目录的 index.html 时,程序使用浏览器 localStorage,适合单人体验。需要跨用户共享时运行 server.cjs,浏览器经 api.js 请求接口,服务把记录保存到 data/posts.json,并用签名 Cookie 区分发布者。两种模式的数据互不迁移;因此“甲发布、乙查找”的场景必须在共享服务模式下验证。
④ 最后收紧提交和状态规则。 发布前检查名称、地点、时间、联系方式等字段;保存失败保留输入,成功后才进入结果页。首页默认展示进行中的信息,搜索将多个关键词和筛选条件按交集匹配。发布者可更正内容,但编辑不能改变记录编号、类型、发布者、发布时间或当前状态;寻物只能结束为“已找到”,招领只能结束为“已归还”。已结束信息退出默认列表,仍可在历史搜索中查看。
2. 关键流程与数据流
总体数据流:用户从页面发起发布、浏览、搜索或管理操作,app.js 将操作交给对应页面模块,再调用共用的业务规则。本机模式把记录存在浏览器 localStorage;共享模式通过 api.js 请求 server.cjs,记录落到 data/posts.json。服务端用签名 Cookie 识别发布者,两种模式的数据相互独立。

发布信息流程:从寻物或招领入口进入同一表单。字段不完整时提示修改并保留已填内容;校验通过后生成编号、初始状态和发布者身份,再尝试保存。只有保存成功才进入成功页,失败时仍留在表单供用户重试。

搜索信息流程:搜索页读取当前模式下的信息,将关键词与类型、类别、地点和是否包含已结束信息组合筛选。多词必须全部命中。读取异常时显示错误,零结果时提示调整条件;有结果则可进入详情核对联系方式。

信息状态转移:信息发布后,寻物处于“寻找中”,招领处于“招领中”。发布者从“我的发布”确认后,分别只能转为“已找到”或“已归还”;非本人、重复结束和跨类型修改都会被拒绝。已结束记录退出默认列表,但可通过历史搜索查看,这样既减少无效联系,也保留处理结果。

已发布内容修改:发布者从“我的发布”选择“编辑内容”后,程序先读取本人记录并回填表单;修改名称、类别、地点、时间、描述或联系方式后点击“保存修改”,校验通过才写入。保存成功返回原来的“我的发布”筛选列表,其他用户重新查询可看到更正后的内容。取消编辑不写入;记录不存在、非本人或保存失败时给出提示。已结束的记录也能更正内容,但不会重新变为进行中。
3. 关键代码片段
发布校验:js/manage.js 将两类发布共用的规则集中在 validate。类型和联系方式类型必须在允许范围内;名称、地点、联系方式要有内容且不得超过 50 字。时间还会检查日历日期是否有效,避免只靠浏览器表单的表面格式判断。
const errors = [];
if (!data || !['lost', 'found'].includes(data.type)) errors.push('请选择寻物或招领类型');
if (!data || typeof data.contactType !== 'string' ||
!Object.hasOwn(Posts.CONTACT_TYPES, data.contactType)) {
errors.push('请选择微信、QQ 或手机号作为联系方式类型');
}
const fields = [['name', '物品名称', 50], ['place', '地点', 50], ['contact', '联系方式', 50]];
fields.forEach(function ([key, label, limit]) {
const value = data && data[key];
if (typeof value !== 'string' || !value.trim()) errors.push(label + '不能为空');
else if (value.trim().length > limit) errors.push(label + '不能超过' + limit + '个字符');
});
保存反馈:js/publish.js 提交时暂时锁定表单,防止连续点击产生重复记录。失败时显示具体原因、保留输入并恢复按钮;成功后才清空表单并导航到对应记录的成功页。以下节选与当前实现一致:
setBusy(true);
const result = await CampusManage.savePost(data);
if (!result.ok) {
setBusy(false);
showError(result.errors.join(' '));
field('error').focus();
return;
}
reset();
form.dataset.saved = '1';
if (location.hash === route) CampusUI.navigate('success?id=' + encodeURIComponent(result.post.id));
else CampusUI.toast('信息已发布,可在我的发布中查看。');
逐项缺项提示:发布与编辑使用同一套表单检查。点击提交后,页面汇总所有缺失或无效的字段,同时在相应输入框下显示具体原因,并将焦点移到第一处错误;用户补齐内容后提示随输入更新。
const invalid = [];
names.forEach(function (name) {
const text = fieldError(name);
markField(name, text);
if (text) invalid.push({ name, text });
});
showError(invalid.length ? '请完善以下内容:' + invalid.map(item => item.text).join(';') + '。' : '');
if (focus && invalid.length) field(invalid[0].name).focus();
return invalid.length === 0;
搜索与筛选:下面节选自 js/posts.js 的 filterPosts。terms.every(...) 保证多个词同时命中;结束状态、类型、类别与地点条件按交集生效,最后按发布时间排序。
const place = String(options.place || '').trim().toLowerCase();
const terms = String(options.keyword || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
return posts.filter(function (post) {
const searchable = [post.name, post.category, post.place, post.desc].join(' ').toLowerCase();
return (options.includeFinished || !isFinished(post)) &&
(!options.type || options.type === 'all' || post.type === options.type) &&
(!options.category || post.category === options.category) &&
(!place || post.place.toLowerCase().includes(place)) &&
terms.every(term => searchable.includes(term));
}).sort(function (a, b) {
return timestamp(b.createdAt || b.time) - timestamp(a.createdAt || a.time);
});
状态维护:js/manage.js 先检查记录是否存在、是否属于当前发布者、是否已经结束,再按寻物/招领限制目标状态。共享模式还会在 server.cjs 的接口层再次核对发布者,避免仅靠页面按钮控制权限。
if (!post) return failure('这条信息不存在或已被删除');
if (post.ownerId !== identity.userId) return failure('只能操作本人发布的信息');
if (Posts.isFinished(post)) return failure('信息已结束,无需重复操作');
if (newStatus !== (post.type === 'lost' ? '已找到' : '已归还')) {
return failure('不允许修改为该状态');
}
post.status = newStatus;
return writePosts(records.posts, storage);
本人编辑与字段保护:js/manage.js 在修改前重新检查记录与发布者身份,沿用发布时的字段校验。构造新记录时先保留原信息,只覆盖允许更正的内容,因此 ID、发布者、发布时间、类型和结束状态保持原样;共享接口对 PUT 请求再次检查身份。
if (!post) return failure('这条信息不存在或已被删除');
if (post.ownerId !== identity.userId) return failure('只能编辑本人发布的信息');
if (data && data.type !== undefined && data.type !== post.type) {
return failure('编辑时不能改变寻物或招领类型');
}
const errors = validate({ ...data, type: post.type });
if (errors.length) return { ok: false, errors };
const updated = {
...post, name: data.name.trim(), category: (data.category || '').trim(),
place: data.place.trim(), time: data.time, desc: (data.desc || '').trim(),
contactType: data.contactType, contact: data.contact.trim()
};
共享数据与服务端权限:共享模式下,server.cjs 将记录先写入临时文件,再替换正式数据文件。修改或删除前,还要用当前签名 Cookie 对应的身份与记录的 ownerId 比对;即使有人绕过页面按钮直接请求接口,也不能操作其他人的记录。
const temporary = path.join(dataDir, '.posts-' + crypto.randomUUID() + '.tmp');
try {
fs.writeFileSync(temporary, value, { flag: 'wx' });
fs.renameSync(temporary, recordsPath);
} finally {
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
}
if (found.post.ownerId !== userId) {
return json(res, 403, { ok: false, errors: ['只能操作本人发布的信息'] });
}
4. 程序运行成果
以下截图来自 Chrome 中运行的正式程序,使用隔离的虚构演示记录;它们展示的是代码实现后的页面,不是上次的墨刀原型。截图里的联系方式仅供展示。
首页:顶部提供搜索入口与寻物、招领两个发布入口;列表显示进行中的记录,并可按信息类型、物品类别和地点缩小范围。蓝绿两色标签帮助区分寻物与招领。

发布页面:选择寻物或招领后填写名称、地点、时间与联系方式。必填项带红色星号,物品信息和联系信息分组呈现,右侧给出描述特征和填写地点的建议。

发布成功页面:保存后展示本次发布摘要,并提供“查看我的发布”“查看这条信息”、返回首页和再发一条等下一步操作,方便继续管理线索。

搜索结果页面:以“雨伞”和“图书馆”为条件时,页面显示命中数量及匹配卡片,并保留当前条件;点击卡片即可进入详情。

我的发布页面:只列出当前发布者的记录,上方统计全部、进行中和已结束数量。每条记录都有“编辑内容”入口;进行中的寻物可标记为“已找到”,已归还的招领记录不再显示结束按钮,但仍可更正内容。

编辑信息页面:从本人记录进入后,原内容自动回填。信息类型显示为不可修改,名称、地点、时间、描述和联系方式可以更正;保存后返回“我的发布”,取消则放弃本次改动。截图使用隔离的虚构账号。

四、附加特点设计与展示
1. 组合筛选与多关键词查询
首页可按寻物/招领、类别和地点缩小范围;搜索支持用空格分隔多个关键词。例如输入“雨伞 图书馆”,只有内容同时包含两个词的记录才会出现。找不到结果时页面给出空状态提示;需要核对历史时,可显式包括已结束信息。实现集中在上述 filterPosts,无需为每种筛选组合各写一套逻辑。
无结果提示:输入没有匹配记录的关键词时,页面明确显示“找到 0 条信息”,并建议尝试其他类别、地点或关键词。

2. 联系方式一键复制及受限提示
详情页直接显示联系类型与账号,减少额外点击。用户点击“复制联系方式”时,程序尝试调用浏览器剪贴板;若浏览器限制复制,就选中只读输入框中的原始账号,提示用户手动复制。这样在直接打开本地 HTML 的场景里也能继续完成联系步骤。
const copied = await copyText(contact, clipboard);
if (copied) CampusUI.toast('联系方式已复制');
else {
input.focus();
input.select();
CampusUI.toast('未能自动复制,已选中联系方式,请手动复制。');
}
详情展示:信息详情列出丢失时间、地点、类别、描述和发布者留下的联系类型与账号;复制受限时可选中账号手动复制。

3. 本人编辑与明确的缺项反馈
发布后发现地点或联系方式写错时,原来的流程只能删除后重发,容易让已看到旧信息的同学失去线索。现在发布者可以直接更正内容,原记录的编号、类型、状态和发布者保持不变;已结束记录也可修改描述或联系方式,但不会重新进入默认列表。页面保存失败时保留修改稿,重新提交不会产生另一条记录。
发布与编辑还共用逐项校验:除了顶部的错误摘要,空白输入框会出现红色边框和具体提示,焦点移到首个缺项。下图故意清空“物品名称”后点击“保存修改”;页面阻止写入,并明确指出要补哪一项。

五、目录说明和使用说明
1. 目录组织
102400412-102401111/
├── .github/
│ └── workflows/
│ └── tests.yml GitHub Actions 自动测试
├── css/
│ └── app.css 全站样式与响应式布局
├── html/ 上次作业的 HTML 原型
├── js/
│ ├── app.js 路由、导航与页面刷新
│ ├── api.js 共享服务请求
│ ├── runtime-config.js 运行模式配置
│ ├── posts.js 数据格式、查询与状态规则
│ ├── manage.js 发布、本人编辑与状态维护规则
│ ├── home.js 首页浏览与筛选
│ ├── search.js 关键词搜索
│ ├── detail.js 详情与联系方式
│ ├── publish.js 发布页与共用表单校验
│ ├── edit.js 编辑页回填与保存
│ └── my.js 我的发布与本人操作
├── resource/ 原型图、流程图与程序截图
├── tests/
│ ├── examples.test.cjs 12 个正文代表用例
│ ├── posts.test.cjs 浏览、搜索与详情单元测试
│ ├── manage.test.cjs 发布、编辑、身份与状态单元测试
│ ├── server.test.cjs HTTP 接口与持久化测试
│ ├── browser.test.cjs 本机 Chrome 页面场景
│ └── shared-browser.test.cjs 双发布者 Chrome 场景
├── index.html 正式 Web 入口
├── server.cjs 共享 HTTP 服务与文件存储
├── README.md 项目介绍与运行方法
└── 测试说明.md 用例设计与测试命令
完整的文件与操作说明也写在仓库的 README 中。data/ 是共享服务运行时生成的数据与身份文件,不随仓库提交。
2. 测试人员如何运行
直接打开网页:下载仓库全部文件并保持目录结构,用 Google Chrome 打开根目录的 index.html。不需安装依赖,可在同一浏览器中体验发布、搜索、详情、本人编辑与状态维护。此模式的数据只保存在本机浏览器,不能拿两个不同浏览器来验证共享。
验证多人共享:安装 Node.js 24,在仓库根目录运行:
node server.cjs
保持终端运行,用 Chrome 打开 http://127.0.0.1:3000。可用普通窗口和隐身窗口分别模拟发布者甲与浏览者乙:甲发布,乙刷新或重新搜索后查看详情;甲修改物品描述或结束记录,乙再次查询后看到更新。普通窗口内的多个标签页共用同一发布者身份。停止服务时回到启动它的终端按 Ctrl+C。项目没有 npm install 步骤,服务首次运行会创建数据文件;共享数据与本机模式数据彼此独立。
六、单元测试
1. 测试工具的选择与使用
业务模块 js/posts.js 和 js/manage.js 可以直接在 Node.js 中导入,因此采用 Node.js 24 内置的 node:test 组织用例、node:assert/strict 比较实际结果与预期结果,不需要安装额外测试框架。单元测试直接调用正式业务函数;HTTP 接口和页面交互分别交给 tests/server.test.cjs 与 Chrome 场景测试,避免把“页面能显示”误当成业务规则已验证。
编写一个用例时,先找出待测函数的输入、返回值和判断分支;再准备独立数据(Arrange),调用函数(Act),最后断言返回结果与存储是否改变(Assert)。例如测试“名称过长”时,既要检查返回失败,也要检查原记录没有被覆盖。新增用例后可先只运行对应文件,再运行全部测试。项目的 测试说明 记录了更完整的运行方法。
# 只运行正文选出的 12 个代表用例
node --test tests/examples.test.cjs
# 单独定位 SC08 状态转换用例
node --test --test-name-pattern=SC08 tests/examples.test.cjs
# 运行全部业务单元测试
node --test tests/examples.test.cjs tests/posts.test.cjs tests/manage.test.cjs
# 运行全部测试(另含 HTTP 与 Chrome 场景)
node --test
2. 测试数据与白盒用例设计
代表用例位于 tests/examples.test.cjs。每条用例新建一个内存存储,互不借用状态;input() 生成完整合法的基准信息,测试某一分支时只覆盖相关字段。时间与联系方式使用固定的虚构值,避免当天日期和个人数据影响结果。以下是测试文件中的数据准备代码:
function memoryStorage() {
const values = new Map();
return {
getItem(key) { return values.has(key) ? values.get(key) : null; },
setItem(key, value) { values.set(key, String(value)); }
};
}
function input(fields = {}) {
return {
type: 'lost', name: '黑色雨伞', category: '雨伞', place: '图书馆',
time: '2026-10-08T09:00', desc: '银色伞柄',
contactType: 'wechat', contact: 'TEST_ONLY_ACCOUNT', ...fields
};
}
按照正常路径、条件分支、边界值与异常路径选取了 12 个代表用例。表中的“覆盖点”说明为何构造这组数据;用例代码均可在仓库中直接运行。
| 编号 | 测试对象 | 构造数据与预期 | 白盒覆盖点 |
|---|---|---|---|
| SC01 | 发布寻物 | 合法字段、名称带空格;保存后名称去空格、状态为“寻找中” | 正常写入与初始状态 |
| SC02 | 发布招领 | 连续发布两条;状态为“招领中”,ID 不重复 | 信息类型分支与编号生成 |
| SC03 | 必填校验 | 名称、地点、联系方式分别为纯空格;拒绝且不写入 | 三个必填条件的失败分支 |
| SC04 | 联系类型 | 类型缺失、空字符串和非法值;均拒绝 | 枚举值校验 |
| SC05 | 名称长度 | 50 字通过、51 字拒绝且旧数据不变 | 长度边界与失败保护 |
| SC06 | 搜索筛选 | 多词加类型、类别、地点;只留下全部条件命中的记录 | 组合条件的交集与无结果分支 |
| SC07 | 详情联系 | 微信、QQ、手机号与未知 ID;标签和账号正确,未知返回空 | 联系类型及记录查找分支 |
| SC08 | 寻物状态 | 本人标记已找到;默认隐藏、历史可见,重复标记失败 | 合法转移与重复结束分支 |
| SC09 | 招领状态 | 先错误标为已找到,再正确标为已归还 | 跨类型拒绝与合法转移 |
| SC10 | 发布者权限 | 切换为另一身份后更新或删除;原数据不变 | 所有者不匹配分支 |
| SC11 | 损坏数据 | 存储中写入非法 JSON;拒绝发布且保留原文 | 读取异常路径 |
| SC12 | 写入失败 | 模拟存储已满;发布和状态更新失败,旧记录不变 | 写入异常路径 |
12 个代表用例之外,tests/posts.test.cjs 和 tests/manage.test.cjs 继续检查非法日期、关键词排序、旧记录兼容、本人编辑、已结束内容更正、越权修改及失败重试。编辑相关测试特别比较修改前后的 ID、发布者、发布时间和状态,防止“内容更正”意外变成新发布或重新开启信息。
3. 部分测试代码与断言
边界值: SC05 只改变名称长度,并在失败后比对存储原文,验证 51 字的输入不会破坏已有记录。
test('SC05 名称长度边界:50 字成功,51 字失败且旧记录不变', () => {
const storage = memoryStorage();
assert.equal(Manage.savePost(input({ name: '伞'.repeat(50) }), storage).ok, true);
const before = storage.getItem(Manage.POSTS_KEY);
assert.equal(Manage.savePost(input({ name: '伞'.repeat(51) }), storage).ok, false);
assert.equal(storage.getItem(Manage.POSTS_KEY), before);
});
状态分支: SC08 同时检查合法结束、默认查询、历史查询和重复结束,防止只验证按钮返回成功,却漏掉列表中的实际效果。
test('SC08 本人找到寻物:结束后默认隐藏,历史查询仍可见', () => {
const storage = memoryStorage();
const post = Manage.savePost(input(), storage).post;
assert.equal(Manage.updateStatus(post.id, '已找到', storage).ok, true);
const records = Manage.readPosts(storage).posts;
assert.deepEqual(Posts.filterPosts(records), []);
assert.equal(Posts.filterPosts(records, { includeFinished: true })[0].status, '已找到');
assert.equal(Manage.updateStatus(post.id, '已找到', storage).ok, false);
});
本人编辑:tests/manage.test.cjs 还覆盖了已结束信息的内容更正。编辑成功后状态仍为“已归还”,尝试改变寻物/招领类型会失败,存储内容保持原样。
test('已结束信息可以更正内容,编辑不会重新开启或改变类型', () => {
const { storage, post } = seed({ type: 'found' });
assert.equal(Manage.updateStatus(post.id, '已归还', storage).ok, true);
const result = Manage.updatePost(post.id, input({ type: 'found', name: '更正后的名称' }), storage);
assert.equal(result.ok, true);
assert.equal(result.post.status, '已归还');
const raw = storage.getItem(Manage.POSTS_KEY);
assert.equal(Manage.updatePost(post.id, input({ type: 'lost' }), storage).ok, false);
assert.equal(storage.getItem(Manage.POSTS_KEY), raw);
});
4. 执行结果与测试范围
在项目根目录实际运行 node --test,结果为 136 项通过、0 失败、0 跳过。其中 79 项为业务单元测试,18 项为真实 HTTP 接口测试;本机与共享 Chrome 流程共 37 个场景,另有 2 个浏览器测试父项。单独运行上述三个业务单元测试文件,结果为 79 项通过。
ℹ tests 136
ℹ pass 136
ℹ fail 0
ℹ skipped 0
业务单元测试使用内存存储,验证函数的分支和数据不变量;HTTP 测试使用临时文件与真实请求检查持久化、Cookie 权限和跨用户访问;Chrome 场景检查表单缺项定位、路由跳转、编辑回填、失败重试与窄屏操作。GitHub Actions 在推送和 Pull Request 等触发时运行单元与接口测试,完整 Chrome 回归在本地运行。真实跨设备网络与公网部署尚未在本次测试中验证。
七、GitHub 代码签入记录
每个功能完成并自测后至少签入一次,提交信息按功能命名。具体记录可查看 GitHub Commits 页面。

八、结对过程
我们沿用上次原型作业的分工,先确定两条主要操作链路,再分别实现页面。龚巍斌负责首页浏览与筛选、关键词搜索、详情与联系方式,以及后续的共享服务整合、本人编辑、表单缺项提示和自动化测试;郑瀚负责寻物与招领发布、发布成功页、“我的发布”、状态维护与删除等页面和交互。
两部分完成后,我们按“发布一条信息—另一位用户搜索并查看详情—发布者修改或结束信息—再次查询”的顺序衔接页面,检查字段、跳转和状态是否一致。集成时重点核对发布表单保存的名称、地点、联系方式能否在卡片和详情页正确显示;对发现的问题分别修改,再运行相关测试并复查完整流程。
九、遇到的问题及解决方法
| 具体问题 | 解决过程与验证 |
|---|---|
| 甲发布信息后,乙在自己的浏览器中搜不到 | 最初的数据保存在各自浏览器的 localStorage,只能在同一浏览器内查看。后来接入 server.cjs 和 js/api.js,使两端读取同一份记录;用独立浏览器身份验证乙能搜索到甲的发布,甲仍能管理自己的信息。 |
| 输入不常见的值时,数据校验与图标显示出现漏洞 | 测试中构造了数组形式的类型值,以及 constructor、toString 等类别名。修复时要求类型必须是字符串,读取类别图标时只接受字典自身的属性,并把这些输入加入回归测试。 |
| 编辑一条已发布信息时,不能把身份和状态一起改掉 | 例如把雨伞的地点由教学楼改成食堂,只应更新地点等内容字段。编辑逻辑保留原记录的编号、发布者、发布时间、类型和状态;服务端核对发布者身份,测试也检查这些字段在编辑前后保持一致。 |
| 发布表单缺项时,用户难以判断该补哪里 | 对物品名称、地点等必填项同时给出顶部汇总和字段旁提示,并将焦点移到第一个缺项。浏览器测试检查了错误提示、焦点位置和“未写入无效记录”这三个结果。 |
十、评价队友
值得学习的地方: 郑瀚的工程思维和实现能力值得我学习。他负责寻物、招领发布,发布成功页和“我的发布”,把信息填写、保存、查看、状态更新与删除串成了完整流程,能考虑到一条记录在不同页面之间如何流转。他在页面衔接上也很细心:发布后有明确的成功反馈,用户还能在“我的发布”中继续查看和管理信息。这让我意识到,写好一个功能还要想清楚用户完成这一步后会做什么。
需要改进的地方: 初版更关注功能能否走通,对用户漏填信息时该看到什么提示考虑得不够充分;自测也没有覆盖完缺项、重复提交等情况。后来我们补充了字段提示和相关测试。下次可以在完成主流程后,从首次使用者的角度再走查一次,并检查异常输入和重复操作。
十一、个人总结
上次作业画原型时,我主要考虑页面上有什么按钮、点击后会到哪里。这次真正写程序,才发现原型里的一个“发布成功”背后还要解决记录如何保存、由谁管理、搜索页何时能看到等问题。页面看起来与原型接近,只是第一步;把发布、搜索、详情和状态更新连成同一条可用的数据流程,才算把设计落到实处。
工程中给我印象最深的是跨用户查询。起初信息存在浏览器的 localStorage 中,自己发布、自己搜索都正常,换一个浏览器身份却看不到。这个问题迫使我重新按失主和拾主的真实场景检查系统,后来接入共享服务,并把发布者身份和修改权限放到服务端验证。编辑功能也让我意识到,修改地点或联系方式时,不能顺手改变原记录的编号、类型和状态;这些约束需要在代码中明确写出。
测试改变了我判断“做完了”的标准。只试一次正常发布,很容易漏掉空字段、异常类型值、重复状态更新或非本人修改等情况。我们把这些场景写进单元、接口和浏览器测试,修复少见类别名等问题后还能反复验证旧功能是否受影响。测试帮助我们找出需求和实现之间没有说清的地方,也让我更有把握判断修改是否影响了原有功能。
结对编程让我看到分工之外更重要的是衔接。郑瀚完成发布与管理链路,我完成浏览、搜索和详情链路;两边单独运行时都能操作,放在一起还要统一字段、状态和页面入口。一起按“发布—搜索—查看—更新”走查后,我更清楚地理解了对方页面需要什么数据,也发现自己应当在动手前把接口约定得更具体。下次合作,我会更早交换可运行的页面和自测结果,让问题在集成前就暴露出来。

浙公网安备 33010602011771号