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

项目 内容
这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/2026-01SoftwareEngineeringandSoftwareEngineeringPractice
这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/2026-01SoftwareEngineeringandSoftwareEngineeringPractice/homework/16745
这个作业的目标 完成「校园失物招领」核心功能的代码实现,实现「发布信息—浏览/搜索—查看详情—联系发布者—更新状态」的完整流程,并完成单元测试、README 文档与 PSP 记录
成员一:学号 / 姓名 102402125 / 黄俊翔
成员二:学号 / 姓名 102402124 / 洪豊帧
原型工具 / 在线原型 墨刀(Modao)· [https://modao.cc/proto/5AmeweAStm2iuwqkXScZBn/sharing?view_mode=read_only&screen=rbpVWVSUILF2FBjjF](第一次作业使用)
开发工具 / 技术栈 原生 HTML + CSS + JavaScript + localStorage(纯前端,零依赖)

一、结对信息


二、具体分工

成员 分工内容
黄俊翔(102402125) 核心业务逻辑层 model.js、存储层 store.js、单元测试设计(21 个用例 + 测试运行器)、README 文档
洪豊帧(102402124) 四个页面 UI(首页/发布/详情/我的发布)与对应页面脚本、整体样式 style.css、博客撰写与 PSP 记录

两人通过 Git 协作:由黄俊翔创建仓库,洪豊帧 fork 后以 Pull Request 提交页面代码,双方交叉 Code Review。


三、PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 【30】
Estimate 估计这个任务需要多少时间 30 【30】
Development 开发
Analysis 需求分析(包括学习新技术) 60 【60】
Design Spec 生成设计文档 60 【60】
Design Review 设计复审 30 【30】
Coding Standard 代码规范 30 【25】
Design 具体设计 90 【88】
Coding 具体编码 240 【233】
Code Review 代码复审 60 【30】
Test 测试(自我测试,修改代码,提交修改) 120 【90】
Reporting 报告
Test Report 测试报告 60 【30】
Size Measurement 计算工作量 30 【23】
Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 34 【待填】
合计 870 【733】

说明:预估耗时偏向「具体设计」与「具体编码」,因为本次代码量集中在页面交互与业务逻辑分层上;「测试」环节预留了充足时间用于修正边界情况。实际耗时请两人按真实记录回填,不要与预估完全一致(否则无法体现返工与低估环节)。


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

4.1 代码实现思路(文字描述)

本次作业延续第一次结对作业「校园失物招领」的原型设计,把需求收敛到最核心的闭环:

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

围绕这一流程,我们在架构上做了明确的分层,把「与界面无关的业务规则」和「界面交互」拆开:

  1. 业务逻辑层 model.js:纯函数集合,实现信息校验 validateItem、信息创建 createItem、搜索过滤 searchItems、按 id 查找 getItemById、状态更新 updateItemStatus。这一层不碰 document 也不碰 localStorage,因此可以脱离浏览器单独做单元测试。
  2. 存储层 store.js:封装 localStorage 的读写,负责数据持久化、首次访问的种子数据生成、以及「我的发布」id 跟踪。页面代码只调用 LFStore 提供的接口。
  3. 页面层(index.html / publish.html / detail.html / mine.html + 各自脚本):只做 DOM 渲染、事件绑定与表单收集,数据操作全部委托给 store.js。

采用纯前端方案(无需后端、无需数据库)的原因是:作业要求「其他同学下载文件后用谷歌浏览器打开 html 即可复现」,纯前端 + localStorage 能最大程度降低环境门槛,且核心流程(发布、搜索、详情、状态更新)都能完整实现。

关于「联系之后如何结束一条信息」:我们补上了流程闭环 —— 发布者在「我的发布」页或自己信息的详情页点击「标记已找到 / 标记已归还」,状态写入 localStorage 后,所有浏览者会在卡片和详情页看到「已找回 / 已归还」徽章,从源头减少重复询问。谁来改、从哪个入口改、其他人在哪看到结果,三条链路都清晰可走。

4.2 关键实现的流程图 / 数据流图

(1)整体业务流程

flowchart LR A[用户] -->|点击发布</br>publish.html| B[填写表单] B --> C{前端 validateItem 校验} C -->|不通过| B C -->|通过| D[store.add</br>createItem 生成 id/状态/时间戳] D --> E[(localStorage<br/>lf_items)] E --> F[首页浏览 + 关键词搜索 + 类型/类别筛选</br>index.html] F -->|点击卡片| G[详情页 detail.html] G -->|一键复制联系方式| H[联系发布者] G -->|是否我的发布| I{我的发布?} I -->|是| J[标记 已找到/已归还</br>updateItemStatus] J --> E I -->|否| K[仅查看状态]

(2)代码分层数据流

flowchart TB subgraph 页面UI层 P1[index.html] -.-> P2[publish.html] P1 -.-> P3[detail.html] P1 -.-> P4[mine.html] end subgraph 页面脚本 S1[main.js] --- S2[publish.js] S1 --- S3[detail.js] S1 --- S4[mine.js] end subgraph 存储层 ST[store.js 封装 localStorage] end subgraph 业务逻辑层 M[model.js 纯函数] end P1 --> S1 --> ST --> M ST <--> L[(localStorage)]

说明:流程图为 mermaid 源码,若博客园未开启 mermaid 渲染,可使用 mermaid.live 或 draw.io 导出 PNG 后,以图片形式插入(建议放 docs/images/,见成果展示部分)。

4.3 重要的/有价值的代码片段及解释

片段一:信息校验(js/model.js)

function validateItem(data) {
  var errors = [];
  if (!data) {
    return { valid: false, errors: [{ field: 'all', message: '数据为空' }] };
  }
  if (TYPE.indexOf(data.type) === -1) {
    errors.push({ field: 'type', message: '请选择信息类型(寻物/招领)' });
  }
  if (!data.title || String(data.title).trim() === '') {
    errors.push({ field: 'title', message: '请填写物品名称' });
  }
  if (CATEGORIES.indexOf(data.category) === -1) {
    errors.push({ field: 'category', message: '请选择物品类别' });
  }
  if (!data.location || String(data.location).trim() === '') {
    errors.push({ field: 'location', message: '请填写遗失/拾取地点' });
  }
  if (!data.time || String(data.time).trim() === '') {
    errors.push({ field: 'time', message: '请填写时间' });
  }
  if (!data.contact || String(data.contact).trim() === '') {
    errors.push({ field: 'contact', message: '请填写联系方式' });
  }
  return { valid: errors.length === 0, errors: errors };
}

解释:校验返回 { valid, errors } 而不是直接抛异常,便于页面层一次性拿到所有缺失字段并高亮。这是把「业务规则」下沉到纯函数、让界面只需按结果渲染的关键设计,也是单元测试的重点对象。

片段二:搜索过滤(js/model.js)

function searchItems(items, opts) {
  opts = opts || {};
  var keyword = (opts.keyword || '').toString().trim().toLowerCase();
  var list = (items || []).filter(function (it) {
    if (opts.type && it.type !== opts.type) return false;
    if (opts.category && opts.category !== '全部' && it.category !== opts.category) return false;
    if (opts.status && it.status !== opts.status) return false;
    if (keyword) {
      var haystack = [it.title, it.category, it.location, it.description]
        .join(' ').toLowerCase();
      if (haystack.indexOf(keyword) === -1) return false;
    }
    return true;
  });
  return list.sort(function (a, b) { return b.createdAt - a.createdAt; });
}

解释:搜索支持关键词(标题/类别/地点/描述模糊匹配)+ 类型 + 类别 + 状态的多条件组合过滤,并默认按发布时间倒序。逻辑独立于页面,方便对「命中标题」「命中地点」「无结果」等边界做白盒测试。

片段三:一键复制联系方式(js/detail.js)

function copyText(text) {
  if (navigator.clipboard && navigator.clipboard.writeText) {
    navigator.clipboard.writeText(text).then(function () {
      showToast('联系方式已复制');
    }, function () { fallbackCopy(text); });
  } else {
    fallbackCopy(text);
  }
}
function fallbackCopy(text) {
  var ta = document.createElement('textarea');
  ta.value = text;
  ta.style.position = 'fixed';
  ta.style.opacity = '0';
  document.body.appendChild(ta);
  ta.select();
  try { document.execCommand('copy'); showToast('联系方式已复制'); }
  catch (e) { showToast('复制失败,请手动复制', true); }
  document.body.removeChild(ta);
}

解释:优先用标准 navigator.clipboard,失败或非安全上下文(file:// 协议下 clipboard 可能被禁用)时降级到 execCommand('copy'),保证在「双击 html 文件」的场景下复制也能尽可能工作。


五、附加特点设计与展示

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

  1. 类型 + 类别实时筛选:首页按「寻物/招领」和物品类别(校园卡、钥匙、手机等)双维度一键过滤,贴合「按类别查找」的真实使用场景。
  2. 一键复制联系方式:详情页一键复制,免去手动输入手机号/QQ 的麻烦与出错。
  3. 状态可视化徽章:「进行中 / 已找回 / 已归还」用彩色徽章区分,浏览者一眼识别信息是否已失效,减少无效联系。
  4. 「我的发布」管理中心:集中管理自己发布过的信息并就地标记状态,形成从发布到闭环的完整链路。

意义:这些特点都紧扣「校园内信息分散、查找不便、联系后信息不闭环」的核心痛点,而非为了堆功能而设计。

5.2 实现思路

  • 筛选:main.js 维护 { type, cate, keyword } 状态,chips 点击时更新状态并调用 store.search(opts) 重新渲染,无需刷新页面。
  • 复制:见 4.3 片段三,双通道降级。
  • 状态徽章:model.getStatusText(type, status) 根据类型 + 状态返回「已找到/已归还/进行中」,卡片与详情页共用。
  • 我的发布:发布时 store.myAdd(id) 把 id 记入本机 lf_my_ids;mine.js 读取 store.myItems() 展示并绑定状态更新按钮。

5.3 值得展示的代码片段

// js/mine.js —— “我的发布”就地标记状态
btn.addEventListener('click', function () {
  var id = btn.getAttribute('data-id');
  var act = btn.getAttribute('data-act');
  if (act === 'view') {
    window.location.href = 'detail.html?id=' + encodeURIComponent(id);
  } else if (act === 'done') {
    var r = store.updateStatus(id, 'resolved');
    if (r.ok) {
      var it = store.getById(id);
      showToast('已标记为「' + model.getStatusText(it.type, 'resolved') + '」');
      render();
    }
  }
});

解释:通过 data-act 区分「查看」与「标记完成」两种操作,事件委托后统一绑定,状态更新成功后即时重渲染,界面与数据保持一致。

5.4 实现成果展示

截图 说明
【截图1:首页整体】 展示卡片列表、搜索框、类型/类别筛选 chips
image
【截图2:搜索演示】 输入关键词后过滤结果
image
【截图3:类型/类别筛选】 筛选效果
image
【截图4:详情页】 描述、联系方式、一键复制、标记按钮
image
【截图5:发布页与校验提示】 发布成功 / 必填项高亮提示
image
【截图6:我的发布】 管理列表 + 标记状态
image
【截图7:状态更新后】 「已找到 / 已归还」徽章
image
【截图8:单元测试报告】 test/index.html 全部 21 用例通过
image

六、目录说明和使用说明

6.1 目录组织

campus-lost-found/
├── index.html            # 首页:浏览 + 搜索 + 类型/类别筛选
├── publish.html          # 发布页:发布寻物/招领信息
├── detail.html           # 详情页:查看详情 + 复制联系方式 + 更新状态
├── mine.html             # 我的发布:管理已发布信息、标记状态
├── css/
│   └── style.css         # 全站共享样式
├── js/
│   ├── model.js          # 核心业务逻辑(纯函数,可单元测试)
│   ├── store.js          # localStorage 存储层
│   ├── main.js           # 首页逻辑
│   ├── publish.js        # 发布页逻辑
│   ├── detail.js         # 详情页逻辑
│   └── mine.js           # 我的发布逻辑
├── test/
│   ├── index.html        # 单元测试页面(浏览器直接打开运行)
│   ├── tests.js          # 21 个测试用例 + 轻量测试框架
│   └── run-tests.js      # Node 环境运行(可选)
├── docs/
│   ├── 博客报告.md        # 本文档
│   └── images/           # 截图素材
└── README.md             # 项目说明(目录 + 使用说明)

6.2 测试人员如何运行

  1. 下载(clone)全部文件,保持目录结构不变。
  2. 用谷歌浏览器打开 index.html(双击即可)。
  3. 首次打开自动生成 3 条示例数据,即可体验发布、搜索、详情、状态更新全流程。
  4. 运行单元测试:用谷歌浏览器打开 test/index.html,页面自动展示测试报告。

注意:数据保存在本机 localStorage,不同浏览器/电脑互不相通;「我的发布」需在发布所用的同一浏览器查看。重置数据可在控制台执行 LFStore.clear() 后刷新。


七、单元测试

7.1 测试工具与学习过程(含简易教程)

工具选择:本项目为纯前端零依赖,为了让助教「下载即测」,我们没有引入需要 npm install 的 Mocha/Jest,而是写了一个轻量测试框架(内置 test() 注册用例 + assert 断言 + 浏览器报告页 test/index.html),并另提供 run-tests.js 供有 Node 环境的同学命令行运行。

简易教程(三步新增一个用例):

  1. 打开 test/tests.js,在下方追加:
    test('用例名称:描述你验证的行为', function () {
      var r = model.某函数(参数);
      assert.equal(r.某属性, 期望值, '失败时的提示');
    });
    
  2. 浏览器打开 test/index.html 观察是否 PASS/FAIL。
  3. 若有 Node:命令行执行 node test/run-tests.js 得到同样结果。

7.2 单元测试代码展示

test('validateItem: 缺少 title 报错', function () {
  var r = model.validateItem(sampleItem({ title: '' }));
  assertFalse(r.valid, '缺少标题应失败');
  var fields = r.errors.map(function (e) { return e.field; });
  assertTrue(fields.indexOf('title') !== -1, '应包含 title 字段错误');
});

test('updateItemStatus: 非法状态报错', function () {
  var it = model.createItem(sampleItem()).item;
  var r = model.updateItemStatus([it], it.id, 'deleted');
  assertFalse(r.ok, '非法状态应失败');
  assertEqual(r.reason, 'invalid-status');
});

test('searchItems: 关键词命中地点', function () {
  var items = [
    model.createItem(sampleItem({ title: 'A', location: '图书馆' })).item,
    model.createItem(sampleItem({ title: 'B', location: '操场' })).item
  ];
  var r = model.searchItems(items, { keyword: '图书馆' });
  assertEqual(r.length, 1);
  assertEqual(r[0].title, 'A');
});

被测试函数为 validateItem、createItem、searchItems、getItemById、updateItemStatus、getStatusText、getTypeText。

7.3 构造测试数据的思路与对测试人员「刁难」的考虑

采用白盒测试为主,覆盖每个分支:

  • 等价类 + 边界:validateItem 分别给「完整 / 空对象 / 缺标题 / 非法类型 / 缺联系方式 / 非法类别」,覆盖每个校验分支。
  • c参数组合:searchItems 覆盖「命中标题 / 命中地点 / 无匹配 / 按类型 / 按类别 / 按状态」。
  • 异常路径:updateItemStatus 覆盖「正常 / 非法状态 / 不存在的 id」,getItemById 覆盖「找到 / 找不到」。

如何应对测试人员刁难:我们主动预判了「空字符串与空白串」「非法枚举值」「不存在的 id」「关键词大小写/前后空格」,并在 createItem 中对标题/地点/联系方式做 trim,搜索时对关键词做 toLowerCase(),尽量让边界输入不导致异常。共 21 个用例,全部通过。


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

问题一:file:// 协议下 navigator.clipboard 被禁用

  • 描述:一键复制在直接双击打开 html 时,navigator.clipboard.writeText 因非安全上下文而失败。
  • 尝试:仅用 clipboard API → 失败;改用 document.execCommand('copy')。
  • 是否解决:已解决(双通道降级,见 4.3 片段三)。
  • 收获:纯前端「双击即用」的场景要注意 Web API 的协议限制,需准备降级方案。

问题二:测试用例「按 status 过滤」偶发依赖 id 唯一性

  • 描述:若两条测试数据 id 相同,状态更新会误改对象,导致断言失败。
  • 尝试:确认 genId() 采用「时间戳 + 随机数」保证唯一;并让搜索按 createdAt 倒序稳定排序。
  • 是否解决:已解决。
  • 收获:构造测试数据时 id 必须唯一,否则状态类测试会产生隐蔽的相互影响。

问题三:结对协作中代码合并冲突

  • 描述:黄俊翔负责逻辑层、洪豊帧负责页面层,合入 PR 时对 store.js 接口有过一次冲突。
  • 尝试:双方约定「逻辑层接口先行、页面层只调用接口不直接改实现」,冲突手动审阅后合并。
  • 是否解决:已解决。
  • 收获:协作前先定好分层边界与接口签名,能减少大部分冲突。

九、评价我的队友

值得学习的地方:洪豊帧对页面细节很认真,主动补上了首页卡片到详情、发布按钮到成功提示的跳转交互,并对样式统一性要求高;遇到问题会先自己排查再讨论,减少了无效沟通。

需要改进的地方:提交代码时 commit 粒度偏大,有时一次提交混合了多个功能,建议按「一个功能一次 commit」拆分,便于溯源与 Code Review。

posted @ 2026-10-10 18:08  很具想  阅读(8)  评论(0)    收藏  举报