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 页面。

结对项目的 GitHub 代码签入记录

八、结对过程

我们沿用上次原型作业的分工,先确定两条主要操作链路,再分别实现页面。龚巍斌负责首页浏览与筛选、关键词搜索、详情与联系方式,以及后续的共享服务整合、本人编辑、表单缺项提示和自动化测试;郑瀚负责寻物与招领发布、发布成功页、“我的发布”、状态维护与删除等页面和交互。

两部分完成后,我们按“发布一条信息—另一位用户搜索并查看详情—发布者修改或结束信息—再次查询”的顺序衔接页面,检查字段、跳转和状态是否一致。集成时重点核对发布表单保存的名称、地点、联系方式能否在卡片和详情页正确显示;对发现的问题分别修改,再运行相关测试并复查完整流程。

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

具体问题 解决过程与验证
甲发布信息后,乙在自己的浏览器中搜不到 最初的数据保存在各自浏览器的 localStorage,只能在同一浏览器内查看。后来接入 server.cjs 和 js/api.js,使两端读取同一份记录;用独立浏览器身份验证乙能搜索到甲的发布,甲仍能管理自己的信息。
输入不常见的值时,数据校验与图标显示出现漏洞 测试中构造了数组形式的类型值,以及 constructor、toString 等类别名。修复时要求类型必须是字符串,读取类别图标时只接受字典自身的属性,并把这些输入加入回归测试。
编辑一条已发布信息时,不能把身份和状态一起改掉 例如把雨伞的地点由教学楼改成食堂,只应更新地点等内容字段。编辑逻辑保留原记录的编号、发布者、发布时间、类型和状态;服务端核对发布者身份,测试也检查这些字段在编辑前后保持一致。
发布表单缺项时,用户难以判断该补哪里 对物品名称、地点等必填项同时给出顶部汇总和字段旁提示,并将焦点移到第一个缺项。浏览器测试检查了错误提示、焦点位置和“未写入无效记录”这三个结果。

十、评价队友

值得学习的地方: 郑瀚的工程思维和实现能力值得我学习。他负责寻物、招领发布,发布成功页和“我的发布”,把信息填写、保存、查看、状态更新与删除串成了完整流程,能考虑到一条记录在不同页面之间如何流转。他在页面衔接上也很细心:发布后有明确的成功反馈,用户还能在“我的发布”中继续查看和管理信息。这让我意识到,写好一个功能还要想清楚用户完成这一步后会做什么。

需要改进的地方: 初版更关注功能能否走通,对用户漏填信息时该看到什么提示考虑得不够充分;自测也没有覆盖完缺项、重复提交等情况。后来我们补充了字段提示和相关测试。下次可以在完成主流程后,从首次使用者的角度再走查一次,并检查异常输入和重复操作。

十一、个人总结

上次作业画原型时,我主要考虑页面上有什么按钮、点击后会到哪里。这次真正写程序,才发现原型里的一个“发布成功”背后还要解决记录如何保存、由谁管理、搜索页何时能看到等问题。页面看起来与原型接近,只是第一步;把发布、搜索、详情和状态更新连成同一条可用的数据流程,才算把设计落到实处。

工程中给我印象最深的是跨用户查询。起初信息存在浏览器的 localStorage 中,自己发布、自己搜索都正常,换一个浏览器身份却看不到。这个问题迫使我重新按失主和拾主的真实场景检查系统,后来接入共享服务,并把发布者身份和修改权限放到服务端验证。编辑功能也让我意识到,修改地点或联系方式时,不能顺手改变原记录的编号、类型和状态;这些约束需要在代码中明确写出。

测试改变了我判断“做完了”的标准。只试一次正常发布,很容易漏掉空字段、异常类型值、重复状态更新或非本人修改等情况。我们把这些场景写进单元、接口和浏览器测试,修复少见类别名等问题后还能反复验证旧功能是否受影响。测试帮助我们找出需求和实现之间没有说清的地方,也让我更有把握判断修改是否影响了原有功能。

结对编程让我看到分工之外更重要的是衔接。郑瀚完成发布与管理链路,我完成浏览、搜索和详情链路;两边单独运行时都能操作,放在一起还要统一字段、状态和页面入口。一起按“发布—搜索—查看—更新”走查后,我更清楚地理解了对方页面需要什么数据,也发现自己应当在动手前把接口约定得更具体。下次合作,我会更早交换可运行的页面和自测结果,让问题在集成前就暴露出来。

posted @ 2026-10-09 22:02  Gwbinn  阅读(8)  评论(0)    收藏  举报