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

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

结对成员:102402126 黄炜杰、102402127 金若曦

一、作业相关信息

项目 内容
结对队友博客链接 黄炜杰的博客
本作业博客链接 本作业博客
GitHub 项目地址 https://github.com/X-runner354/102402126-102402127

二、具体分工

成员 负责内容
102402126 黄炜杰 核心业务逻辑设计(core.js 纯函数)、数据结构与数据模型定义、单元测试设计与编写、README 文档、GitHub 仓库管理
102402127 金若曦 页面结构与样式设计(index.html + style.css)、交互逻辑(app.js 渲染与事件)、数据流图与流程图绘制、附加特点(一键复制、多条件筛选)设计

两人通过 Pull Request 协作,代码有进展即签入 GitHub。

三、PSP 表格

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

差异说明:实际耗时比预估多出约 210 分钟,主要集中在「测试」(+60,编写用例时发现局部变量作用域与筛选组合逻辑有 bug,返工修改)、「具体编码」(+60,页面在部分窗口尺寸下排版错位,反复调整样式)、「需求分析」(+30,围绕"联系之后如何结束信息"补充了状态流转设计)。这说明我们对测试与样式适配的难度有所低估,下次应在预估时预留更多测试与返工余量。

四、解题思路描述与设计实现说明

4.1 代码实现思路

需求理解

本次作业的核心是做一个"校园失物招领"产品,必须跑通一条基本的闭环:

发布信息 → 浏览/搜索 → 查看详情 → 联系发布者 → 更新状态

同时明确不做复杂后台、实名认证、即时聊天、地图定位。结合第一次作业原型,我们把范围收敛到最核心的几个能力上:

  1. 发布:区分"寻物(lost)"和"招领(found)"两类信息;
  2. 集中浏览:以卡片列表展示全部信息,徽章区分类型与状态;
  3. 搜索:按物品名称、地点、描述等关键词检索;
  4. 查看详情:展示完整字段与发布者联系方式;
  5. 更新状态:物品找回/归还后,由发布者标记为"已找到"/"已归还"。

技术选型

  • 纯前端 SPA:只用一个 index.html,通过 JavaScript 渲染四个视图(首页/发布/我的发布/详情),测试人员用谷歌浏览器直接打开即可运行,零环境依赖。
  • 数据存储用 localStorage:无需后端,刷新不丢数据,方便测试与演示。
  • 前端分层:把与界面无关的业务逻辑抽成纯函数放入 core.js,界面渲染与事件放入 app.js,二者解耦,从而让核心逻辑能被 Node/Jest 直接单元测试。

数据模型

每条信息用统一的对象表示:

{
  id,          // 唯一标识,generateId() 生成
  type,        // 'lost'(寻物) | 'found'(招领)
  itemName,    // 物品名称
  category,    // 类别:证件/电子产品/文具/衣物/钥匙/生活用品/其他
  location,    // 地点:教学区/图书馆/食堂/操场/宿舍区/其他
  itemDate,    // 遗失/拾取日期
  description, // 详细描述
  contact,     // 联系方式
  publisher,   // 发布者
  status,      // 'open'(进行中) | 'resolved'(已解决)
  createdAt    // 创建时间戳,用于排序
}

4.2 关键流程(数据流图)

整体数据流图:

数据流图

核心业务流程图:

核心业务流程图

图片托管在 GitHub 仓库 docs/images/,通过 jsDelivr CDN 加速引用,国内访问更快、更稳定。

4.3 重要代码片段及解释

(1)信息校验与创建(core.js)

function validateItem(input) {
  const errors = {};
  if (!input.type || (input.type !== TYPES.LOST && input.type !== TYPES.FOUND)) {
    errors.type = '类型必须是寻物或招领';
  }
  if (!input.itemName || !String(input.itemName).trim()) {
    errors.itemName = '物品名称不能为空';
  }
  if (!input.category || !String(input.category).trim()) {
    errors.category = '请选择物品类别';
  }
  if (!input.location || !String(input.location).trim()) {
    errors.location = '地点不能为空';
  }
  if (!input.contact || !String(input.contact).trim()) {
    errors.contact = '联系方式不能为空';
  }
  if (!input.publisher || !String(input.publisher).trim()) {
    errors.publisher = '发布者不能为空';
  }
  return { valid: Object.keys(errors).length === 0, errors: errors };
}

解释:validateItem 把"发布信息是否完整"这一规则集中到一个纯函数里,返回是否合法及具体错误字段。前端只负责展示第一条错误提示,测试也能直接调用它覆盖各种边界(空值、非法类型)。这也是上次作业点评里重点提到的"发布信息不完整要有提示"。

(2)搜索与筛选(core.js)

function searchItems(items, keyword) {
  const kw = String(keyword || '').trim().toLowerCase();
  if (!kw) return items.slice();
  return items.filter(function (item) {
    return [item.itemName, item.category, item.location, item.description, item.publisher]
      .filter(Boolean)
      .some(function (field) { return String(field).toLowerCase().indexOf(kw) !== -1; });
  });
}

解释:搜索使用 toLowerCase 做大小写不敏感匹配,对物品名、类别、地点、描述、发布者等多字段联合检索;关键词为空时直接返回原列表副本,避免误过滤。

(3)状态更新(core.js)

function updateStatus(items, id, newStatus) {
  return items.map(function (item) {
    if (item.id === id) {
      return Object.assign({}, item, { status: newStatus });
    }
    return item;
  });
}

解释:使用不可变更新,不修改原数组而是返回新数组,便于测试断言"原数据不被污染",也让界面渲染逻辑更可预测。

五、附加特点设计与展示

5.1 设计的创意独到之处及意义

  1. 一键复制联系方式:失主/拾主常有"看了一遍又得手动背号码"的烦恼,一键复制电话号码直接写入剪贴板,减少手动输入错误,这正是助教提示过的加分点,也贴合"减少无效联系"的产品目标。
  2. 多条件组合筛选:除了"寻物/招领"标签,还支持按类别、地点、状态三重下拉组合筛选,用户能快速缩小范围,比单一关键词搜索更贴近真实使用。
  3. 「我的发布」自管理:以发布者姓名为维度集中展示自己的信息,直接提供"标记已解决 / 删除"按钮,把"联系之后如何结束这条信息"这条流程补完整——谁改、从哪改、别人在哪看到结果都清晰可见。
  4. 状态文案自适应:statusLabel 根据类型自动切换文案——寻物解决显示"已找到"、招领解决显示"已归还",避免"已找到"用在招领物品上的语义混乱。

5.2 实现思路

一键复制实现上考虑了兼容性:优先用 navigator.clipboard.writeText(现代浏览器 API,但 file:// 协议下可能受限),失败时回退到 document.execCommand('copy'),保证在本地直接打开文件也能复制成功。

5.3 代码片段及解释

function copyText(text) {
  if (navigator.clipboard && navigator.clipboard.writeText) {
    navigator.clipboard.writeText(text).then(function () {
      showToast('联系方式已复制到剪贴板');
    }).catch(function () { legacyCopy(text); });
  } else {
    legacyCopy(text);
  }
}

function legacyCopy(text) {
  var ta = document.createElement('textarea');
  ta.value = text;
  ta.style.position = 'fixed';
  ta.style.opacity = '0';
  document.body.appendChild(ta);
  ta.select();
  var ok = false;
  try { ok = document.execCommand('copy'); } catch (e) {}
  document.body.removeChild(ta);
  showToast(ok ? '联系方式已复制到剪贴板' : '复制失败,请手动复制');
}

解释:在无后端、直接 file:// 打开的场景下,新旧的 Clipboard API 行为不一致,这里做了双通道降级,保证任何环境都能成功复制。

5.4 实现成果展示

首页集中展示寻物与招领信息,徽章区分类型与状态,支持关键词搜索与多条件筛选。

首页

输入「校园卡」后仅保留匹配结果,其余信息被过滤。

搜索

寻物与招领共用一张发布表单,只差一个类型选项,必填项缺失会提示。

发布信息

详情页展示完整字段与发布者联系方式,支持一键复制。

信息详情

发布者在「我的发布」中按姓名筛选自己的信息,并标记「已找到 / 已归还」或删除。

我的发布

六、目录说明和使用说明

6.1 目录是如何组织的

102402126-102402127/
├── index.html          # 入口页面(单页应用,含首页/发布/我的发布/详情四类视图)
├── css/
│   └── style.css       # 全局样式:主题变量、卡片、表单、徽章、响应式
├── js/
│   ├── core.js         # 核心业务逻辑(纯函数,无 DOM 依赖,便于单元测试)
│   └── app.js          # 界面逻辑:渲染、事件、localStorage 读写、视图切换
├── test/
│   └── core.test.js    # Jest 单元测试(21 个用例)
├── docs/
│   └── images/         # 数据流图、业务流程图及界面截图(首页/发布/搜索/详情/我的发布)
├── package.json        # 项目描述与 Jest 测试脚本
├── CONTRIBUTING.md     # 提交规范与协作流程说明
└── README.md           # 目录与使用说明

设计原则:界面层与业务层分离。core.js 不依赖浏览器 API,可被 app.js(浏览器全局对象)和 Jest(CommonJS)两种方式加载,同一套业务逻辑既服务页面又服务测试,避免逻辑重复。

6.2 测试人员如何运行网页

  1. 将整个项目下载到本地;
  2. 用谷歌浏览器(Chrome)打开 index.html 即可,无需联网、无需部署服务器;
  3. 数据保存在浏览器 localStorage 中,刷新不丢失。

操作路径:首页浏览 → 搜索/筛选 → 点击卡片看详情 → 一键复制联系方式 → 点「发布信息」发布一条 → 到「我的发布」输入自己姓名查询 → 标记「已找到/已归还」或删除。

七、单元测试

7.1 测试工具选择与学习过程(附简易教程)

我们选择 Jest:它零配置、语法直观、社区资料多,且完美支持我们"核心逻辑抽成纯函数"的架构——core.js 用 if (typeof module !== 'undefined' && module.exports) 条件导出,浏览器里当全局对象用,Jest 里直接 require。

简易教程(三步跑起来):

  1. 安装 Node.js;
  2. 在项目根目录 npm install(会自动安装 package.json 里的 jest);
  3. 写测试文件 test/xxx.test.js,用 const LostFound = require('../js/core.js') 引入被测代码;
  4. 运行 npm test 即可看到全部用例结果。

一个最小例子:

test('add 求和', () => {
  expect(1 + 2).toBe(3);
});

断言用 expect(...).toBe(...) / .toEqual(...) / .toThrow(),describe 用来分组。

7.2 单元测试代码展示(说明测试的函数)

针对 core.js 中的 validateItem、createItem、searchItems、filterItems、updateStatus、getItemById、deleteItem、statusLabel、generateId 编写用例,共 21 个,节选如下:

describe('updateStatus / getItemById / deleteItem 状态维护', () => {
  const items = [
    LostFound.createItem(validInput({ itemName: 'A' })),
    LostFound.createItem(validInput({ itemName: 'B', type: 'found' }))
  ];

  test('更新目标条目状态且不修改原数组(不可变)', () => {
    const updated = LostFound.updateStatus(items, items[0].id, LostFound.STATUS.RESOLVED);
    expect(items[0].status).toBe(LostFound.STATUS.OPEN);
    expect(updated[0].status).toBe(LostFound.STATUS.RESOLVED);
    expect(updated[1].status).toBe(LostFound.STATUS.OPEN);
  });

  test('更新不存在的 id 时原样返回', () => {
    const updated = LostFound.updateStatus(items, 'not-exist', LostFound.STATUS.RESOLVED);
    expect(updated).toEqual(items);
  });

  test('getItemById 能找到并返回对象,找不到返回 null', () => {
    expect(LostFound.getItemById(items, items[1].id).itemName).toBe('B');
    expect(LostFound.getItemById(items, 'not-exist')).toBeNull();
  });
});

这段测试验证的是"状态维护"三个最关键的纯函数:updateStatus 是否只改目标条目、是否保持不可变;getItemById 的命中与未命中两种情形;deleteItem 是否正确移除。

7.3 构造测试数据的思路

我们按白盒测试为主,覆盖每个函数的分支与边界:

  • 正常路径:所有字段齐全、能搜索到、能筛选到、能正确定位;
  • 边界输入:空字符串、纯空格、大小写混合、非法 type、不存在的 id、无匹配关键词;
  • 异常路径:非法输入 createItem 抛出异常、toThrow() 断言;
  • 副作用:updateStatus 验证不可变性,确保原数组不被污染。

为了让测试可读、可维护,用 validInput(overrides) 工厂函数构造"标准合法对象",再按需 overrides 覆盖单个字段,避免每个用例重复拼一大段对象。针对助教可能的"刁难",我们把"空字符串、非法类型、组合筛选无结果、大小写、不存在的 id"这类最容易出 bug 的情况都写成了断言。

八、遇到的代码模块异常或结对困难及解决方法

问题描述 做过哪些尝试 是否解决 有何收获
用 file:// 打开时 navigator.clipboard 因非安全上下文失效,无法复制 改用 document.execCommand('copy') 降级方案,并做双通道兜底 是 本地离线场景要考虑浏览器 API 的可用性差异,要做好降级
搜索实现初期遗漏了"大小写不敏感"导致英文物品名搜不到 统一 toLowerCase() 后再 indexOf 匹配 是 搜索务必显式处理大小写与前后空格
状态文案"已找到"对招领物品语义不通 引入 statusLabel 根据 type 动态返回「已找到/已归还」 是 文案应与数据状态绑定统一生成,避免手写多处不一致
结对协作初期没有先约定仓库与目录规范,导致 Push 冲突 提前在 README 约定目录结构,统一 feat:/fix: 提交信息,各自按功能切分支 是 Git 协作要"约定先行",功能切分清楚才能少冲突

九、评价你的队友

值得学习的地方:队友(黄炜杰)负责核心业务逻辑,把与界面无关的部分抽成纯函数(core.js),设计清晰、可测试性好;单元测试用例想得很全(21 个,覆盖空输入、非法类型、不可变性等边界与异常路径),并主动制定了 Git 提交规范和目录结构,让两人的协作和合并顺畅很多。

需要改进的地方:偏重逻辑实现,对界面视觉细节投入较少,初始配色和排版偏朴素,可以多留意一下 UI 观感;确定技术方案时比较坚持自己的做法,沟通中可以多听听对方对交互体验的想法。


全文完

posted @ 2026-10-10 12:38  山冇木兮木有枝  阅读(10)  评论(0)    收藏  举报