软件工程第二次结队作业

结对编程作业:拾光·校园失物招领 Web 项目

项目内容
这个作业属于哪个课程 202601软件工程
这个作业要求在哪里 2026秋软件工程第二次结对作业之程序实现
这个作业的目标 基于第一次结对作业的原型设计,完成核心功能,使“发布信息—浏览或搜索—查看详情—联系发布者—更新状态”的流程清晰、可用
学号 182400143
结对同学博客链接 吴涵彬的博客
GitHub 仓库 292400336-182400143

一、成员与具体分工

成员 实际负责的工作
吴涵彬(292400336) 建立并维护主仓库,提交项目初版与验收说明,合并三项 Pull Request。
张宇天(182400143) 通过 Fork 提交界面与移动端适配,补充目录和使用说明,并重写 README 的使用指南。
双方共同 通过 Pull Request 将改动集成到主仓库,留下可追溯的提交与合并记录。

仓库记录显示,Ther-ux 从 Fork 提交了界面与移动端适配的 PR #1、目录与使用说明的 PR #2,以及重写两份 README 的 PR #3,三项均已合并。

二、PSP 2.1 时间记录

下表按 docs/PSP.md 明细汇总。Development 和 Reporting 是小计;合计只加 Planning、Estimate 和两个小计,避免重复计数。

PSP 阶段 内容 预估(分钟) 实际(分钟)
Planning 计划 18 17
Estimate 估计任务耗时 14 11
Development 开发小计 440 483
Analysis 需求分析与技术学习 43 37
Design Spec 生成设计文档 34 30
Design Review 设计复审 19 23
Coding Standard 代码规范 14 11
Design 具体设计 67 78
Coding 具体编码 172 187
Code Review 代码复审 29 33
Test 测试和修改 62 84
Reporting 报告小计 98 109
Test Report 测试报告 44 51
Size Measurement 计算工作量 15 11
Postmortem 事后总结与改进计划 39 47
合计 570 620

实际比预估多 50 分钟,具体设计和测试环节超出较多。设计时补充了状态维护入口,测试时增加了空结果和非法输入等边界情况。下次会在编码前先列出状态变化与异常路径。

三、解题思路与设计实现

需求和数据结构

校园失物信息散落在聊天记录里,后续搜索和核对状态不方便。我们把任务拆为“发布—检索—查看—联系—更新状态”五步。js/store.js 负责数据校验、搜索、排序、状态和存储;js/app.js 负责路由、渲染与按钮事件。信息字段包括类型、名称、类别、地点、日期、描述、联系人、联系方式、发布者标识、状态和发布时间。

首次打开网页会加载 6 条示例信息。用户发布的数据存入当前浏览器的 localStorage。这是本地演示网页,没有服务端或账号系统;不同设备间不会同步,ownerId 只是演示身份标识,不能作为真实身份认证。

核心流程图

校园失物招领核心流程图

图中发布和浏览是两条可独立进入的路径。表单经 validateDraft 检查,createItem 创建对象,saveItems 保存;首页调用 filterItems 产生列表;本机发布者调用 updateStatus 后再次保存。数据只在当前浏览器的 localStorage 中更新,没有服务端或跨设备同步。

关键代码片段

发布前先查缺失字段,再查类型和文本长度。页面与数据层共用这一校验规则。

function validateDraft(draft) {
  const required = ['type','title','category','location','date',
    'description','contactName','contact'];
  const missing = required.filter((key) => !String(draft?.[key] ?? '').trim());
  if (missing.length) return { ok:false, message:'请完整填写所有必填项。', missing };
  if (!['lost','found'].includes(draft.type))
    return { ok:false, message:'信息类型无效。', missing:['type'] };
  if (String(draft.title).trim().length < 2)
    return { ok:false, message:'物品名称至少填写 2 个字。', missing:['title'] };
  if (String(draft.description).trim().length < 5)
    return { ok:false, message:'物品描述至少填写 5 个字,便于他人辨认。', missing:['description'] };
  return { ok:true, message:'', missing:[] };
}

检索采用多个条件同时满足,关键词在名称、描述、地点和类别中任一命中即可;最终复制数组并按发布时间倒序排列,不改变原始数组。

function filterItems(items, filters = {}) {
  const keyword = normalize(filters.keyword);
  const type = filters.type || 'all';
  const category = filters.category || 'all';
  const location = filters.location || 'all';
  const status = filters.status || 'all';
  return items
    .filter((item) => type === 'all' || item.type === type)
    .filter((item) => category === 'all' || item.category === category)
    .filter((item) => location === 'all' || item.location === location)
    .filter((item) => status === 'all' || item.status === status)
    .filter((item) => !keyword ||
      [item.title,item.description,item.location,item.category]
        .some((field) => normalize(field).includes(keyword)))
    .slice()
    .sort((a,b) => new Date(b.createdAt) - new Date(a.createdAt));
}

更新状态前先检查条目是否存在、演示身份是否匹配。已经解决的信息再次更新时返回副本,以保留原解决时间。

function updateStatus(items, id, actorId = OWNER_ID) {
  const index = items.findIndex((item) => item.id === id);
  if (index < 0) throw new Error('信息不存在。');
  if (items[index].ownerId !== actorId)
    throw new Error('只有发布者可以更新这条信息。');
  if (items[index].status === 'resolved') return clone(items);
  const next = clone(items);
  next[index].status = 'resolved';
  next[index].resolvedAt = nowIso();
  return next;
}

四、附加特点:设计、实现与效果

多条件组合筛选。 用户只记得“大概在图书馆”“可能是雨伞”时,可以同时选择类型、类别、地点、处理状态。filterItems 用四个 .filter() 串联实现条件组合;normalize 去除首尾空格并统一大小写。列表显示匹配条数;没有结果时提供提示和“清空筛选”。

一键复制联系方式。 详情页可以复制发布者主动填写的联系方式,减少手抄错误。实现先尝试 Clipboard API;失败时使用临时文本框回退。

async function copyText(text) {
  try { await navigator.clipboard.writeText(text); showToast('联系方式已复制'); }
  catch (_) {
    const input = document.createElement('textarea');
    input.value = text;
    input.style.position = 'fixed';
    input.style.opacity = '0';
    document.body.appendChild(input);
    input.select();
    document.execCommand('copy');
    input.remove();
    showToast('联系方式已复制');
  }
}

下面是实际页面的运行截图:

信息广场首页

发布信息表单

我的发布与状态

类型、类别和地点组合筛选后,只剩一条符合条件的招领信息:

组合筛选结果

发布“银色U盘”后,详情页展示物品信息和联系方式;点击复制按钮后出现成功提示:

信息详情与联系方式

复制联系方式成功提示

同一条信息在“我的发布”中从“进行中”变为“已找到”,回到首页搜索也能看到更新后的状态:

状态更新前

状态更新后

首页显示更新后的状态

五、目录组织与测试人员使用说明

292400336-182400143/
├─ README.md                         项目功能、运行和使用说明
└─ 校园失物招领-结对编程作业/
   ├─ README.md                     网页目录内的同版使用说明
   ├─ index.html                     页面入口和模板
   ├─ favicon.svg                    标签图标
   ├─ css/styles.css                 页面样式与响应式布局
   ├─ js/store.js                    数据校验、筛选、状态与存储
   ├─ js/app.js                      页面渲染、路由和事件
   ├─ tests/store.test.js            单元测试
   ├─ docs/PSP.md                    时间记录
   ├─ docs/TEST_REPORT.md            测试说明
   ├─ docs/BLOG_DRAFT.md             本文草稿
   ├─ docs/SUBMISSION_CHECKLIST.md   提交检查
   ├─ screenshots/                   页面截图与流程图
   ├─ run-tests.bat                  Windows 测试脚本
   └─ 双击打开网页.bat                Windows 启动脚本

两份 README.md 现按使用者阅读顺序介绍项目功能、目录、运行、使用和测试方法;成员分工、PSP 时间、测试设计与结对协作过程在本博客和 docs/ 中记录。测试人员下载并完整解压项目后,找到包含 index.html 的网页目录,用 Google Chrome 打开该文件即可。网页无需安装依赖、启动服务器或数据库;Windows 也可以双击 双击打开网页.bat。

建议依次检查:搜索“校园卡”→组合筛选→打开详情→发布一条新信息→进入“我的发布”→标记“已找到”或“已归还”→返回首页确认状态。发布信息保存在当前浏览器。要恢复初始 6 条示例数据,可在 Chrome 开发者工具 Application → Local Storage 删除 campus-lost-found-v1 后刷新。

六、单元测试:工具、简易教程和测试数据

我们使用 Node.js 内置的 node:test 和 node:assert/strict,不需要安装其他测试框架。测试目标是可脱离 DOM 运行的 js/store.js。学习时先看基本断言写法,再对照函数的条件分支设计输入和期望结果,最后运行测试并修正失败用例。

简易教程:安装 Node.js 18 或更新版本,进入项目目录运行 node --test tests\store.test.js。测试文件顶部用 require('node:test')、require('node:assert/strict') 引入工具,再引入被测模块。一个用例按“准备输入 → 调用函数 → 断言输出”编写。若断言失败,终端会给出用例名和错误;修复后重新运行全部测试。

以下是两项真实用例。T08 验证类别与地点同时生效;T12 验证非发布者更新状态被拒绝。

test('T08:按类别和地点组合筛选', () => {
  const result = Store.filterItems(Store.seedItems,
    { category:'校园卡/证件', location:'教学楼' });
  assert.equal(result.length, 1);
  assert.equal(result[0].title, '蓝色校园卡');
});

test('T12:非发布者不能修改他人状态', () => {
  const data = [{ ...Store.seedItems[0], id:'other-1',
    ownerId:'other', status:'active' }];
  assert.throws(() => Store.updateStatus(data, 'other-1', 'me'), /只有发布者/);
});

14 个用例分别检查:完整发布、空联系方式、过短描述、创建后的默认状态、名称搜索、描述与地点搜索、类型筛选、类别加地点筛选、状态筛选、本人更新、招领状态文案、非本人更新、发布时间排序、搜索词空格和英文大小写。测试数据覆盖正常输入、边界输入和异常输入。针对可能的“刁难”,还考虑了不存在的搜索词、多条件无结果和重复更新;其中空结果有页面反馈,重复更新有实现分支,现有自动化用例尚未单独覆盖这两项,应如实区分。

本地执行上述命令得到 14 个通过、0 个失败。另有 Chrome 手工走查记录:发布、详情、复制联系方式、更新状态、返回首页核对。手工走查验证页面操作,单元测试验证数据函数。

下图根据本机实际运行 node --test tests/store.test.js 的标准输出排版生成,保留了 14 项用例及通过统计;它是运行结果图,不是终端窗口的原生截屏。

14项单元测试运行结果

七、GitHub 代码签入与结对协作

主仓库:endlessmaybe/292400336-182400143;Fork:Ther-ux/292400336-182400143。PR #1:界面与移动端适配 包含 3 次提交,PR #2:目录与使用说明 和 PR #3:README 使用指南 各包含 1 次提交,均已合并。提交信息按改动性质使用 feat:、docs:,例如 feat: refresh campus lost and found interface、docs: add directory and usage guide for testers 和 docs: present project usage without coursework details。

下图截自 GitHub 主仓库的 Commits 页面,可见双方提交、提交信息及三次合并记录;三张 PR 截图显示了已合并状态。

GitHub代码签入记录

PR1已合并

PR2已合并

PR3已合并

八、遇到的问题、尝试与收获

问题一:联系后信息仍显示进行中。 只有发布、搜索和详情时缺少结束入口。我们增加“我的发布”,并允许本机发布者在详情页更新状态;更新后保存到 localStorage,首页和详情重新显示“已找到/已归还”。本地演示范围内已解决。收获是设计时要画出信息从创建到结束的完整状态变化。

问题二:多条件搜索容易出现看不懂的空结果。 我们统一处理“全部”条件,给关键词去空格并规范英文大小写,还加入匹配条数、空结果提示和清空筛选按钮。组合筛选与大小写由单元测试核对,现有页面已能清楚反馈无结果。收获是正常路径和异常路径应一起设计。

协作中的版本衔接:界面改动从 Fork 通过 PR 合入主仓库后,目录说明和 README 也需要跟随最终页面更新。我们用后续两次文档 PR 补齐运行步骤与测试人员使用说明。这个过程提醒我们,功能合并后还要同步检查文档与界面是否一致。

九、队友评价

吴涵彬在主仓库提交项目初版、完善验收说明,并合并了三项 Pull Request。主仓库中的这些记录让功能改动和文档补充都有明确的集成过程,方便我们回看每一阶段的成果。

下次结对时,我希望我们能在开始编码前把验收标准和截图要求写得更具体;每次合并后再核对界面、说明文档与测试结果,减少最后集中整理材料的时间。我也会按这个方式改进自己的提交。

十、总结

项目完成了发布、检索、查看联系方式和更新状态的完整本地流程,并以 14 个单元测试和 Chrome 手工走查核对关键路径。如果继续用于真实校园场景,需要服务端存储、账号身份验证和跨设备同步。

posted @ 2026-10-07 21:10  Ofseven  阅读(23)  评论(0)    收藏  举报