软件工程第二次结对作业

软件工程第二次结对作业之程序实现

项目 内容
课程 H202601软件工程与软件工程实践
作业要求 2026秋软件工程结对作业(第二次之程序实现)
作业目标 将校园失物招领原型实现为可运行的 Web 应用,完成核心业务流程,并通过 GitHub 协作、PSP 和单元测试实践结对开发、版本管理与软件测试
学号姓名 102401614 林志涛、 102401625 朱铮睿、
GitHub 仓库 https://github.com/Ruiii1124/102401625-102401614
队友博客 https://www.cnblogs.com/Ruiiiiiiiiiii/p/23190772
原型设计稿 https://www.figma.com/design/uqIdAMnXmbEUCrGxY2w4w0

一、项目简介与结对分工

1.1 项目背景与目标

校园卡、钥匙、耳机和雨伞等物品丢失或被捡到后,学生通常通过班级群、宿舍群或朋友圈发布信息。这些信息容易分散、被新消息覆盖,失主和拾物者也可能不在同一个群聊中。

本项目设计校园失物招领系统,将寻物和招领信息集中展示,帮助用户完成信息发布、搜索、详情查看、联系方式获取、状态修改等操作。项目同时关注手机端使用体验,通过统一的卡片布局、底部导航和状态颜色,让用户能够快速完成核心流程。

核心业务流程为:发布信息 → 浏览/搜索 → 查看详情 → 联系发布者 → 更新状态

1.2 结对分工

阶段 朱铮睿(102401625) 林志涛(102401614) 合作方式
第 1 阶段:需求分析 分析首页浏览、搜索、筛选和详情页需求 分析发布、我的发布和状态管理需求 共同阅读作业要求,确定用户角色和功能范围
第 2 阶段:数据与接口设计 设计数据结构与 localStorage 存储方式 确定页面需要的字段和接口 共同确定字段名称、ownerId 规则和模块接口
第 3 阶段:核心页面开发 完成首页、搜索、详情、发布页和数据层 完成状态语义统一、测试基线修复、README 整理 分别在功能分支开发,通过 GitHub PR 合并
第 4 阶段:功能完善 我的发布按发布者区分、搜索特殊字符修复、分类筛选 复制反馈修复、地点筛选、图片上传、54 个测试 在 PR 中互相审查,合并后回归测试
第 5 阶段:测试与修复 单元测试编写、数据层测试用例 状态相关测试用例、图片回归用例 共同使用 Chrome 测试,处理跨页面数据同步
第 6 阶段:协作提交 仓库维护、PR 合并、博客主体撰写 PR 提交、README 更新、文档校对 共同完成 commit、PR、冲突处理、PSP 和博客

1.3 PSP 表格

PSP2.1 阶段 中文说明 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 25 35
Estimate 估计任务所需时间 20 25
Development 开发阶段汇总 480 620
Analysis 需求分析(包括学习图片上传、localStorage 等技术) 40 55
Design Spec 生成设计文档 30 35
Design Review 与队友复核字段、接口和页面跳转设计 25 30
Coding Standard 制定 HTML、CSS、JavaScript 和 Git 提交规范 15 15
Design 具体设计 40 50
Coding 编写数据层、首页、搜索、发布、详情、状态功能 220 300
Code Review 检查代码结构、文件范围和公共接口调用 40 50
Test 自测发布、搜索、状态修改和刷新流程 70 90
Reporting 编写报告和博客内容 80 90
Test Report 整理单元测试结果 40 45
Size Measurement 统计负责文件、功能点和提交规模 15 15
Postmortem & Process Improvement Plan 总结接口、合并和测试问题,提出改进措施 25 30
合计 690 855

实际耗时高于预估,主要增加在具体编码、测试和代码复审阶段。开发过程中需要多次进行 Git 分支同步、接口联调和异常排查,图片压缩、搜索特殊字符、PowerShell 执行策略等问题都占用了额外时间。并且由于两人工作时间的不同步,PR往往不能及时通过,导致两人的工作会陷入阶段停滞。以后需要先分工协商好,并理清开发流程。

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

2.1 代码实现思路

本项目采用纯前端方案:原生 HTML、CSS 和 JavaScript,数据存储在浏览器 localStorage 中,无需后端服务。这样做的原因是作业明确不要求复杂后台,且纯前端便于助教直接打开网页复现。

项目将数据处理和页面显示分离。js/data.js 作为公共数据模块,负责统一处理物品数据的读取、保存、搜索、状态更新和发布者校验。首页、搜索页、详情页、发布页和“我的发布”不直接维护数据,而是通过调用 data.js 的函数完成操作。

每条物品信息使用统一的数据结构,包括 id、type、name、category、location、date、description、contact、image、status、ownerId 和 createdAt 等字段。发布时间用 createdAt 保存绝对时间,页面显示时再格式化为“10月6日”等文字。

系统主要处理流程如下:

  • 页面加载时从 localStorage 读取数据;
  • 首页按类型筛选并按发布时间排序;
  • 搜索页根据关键词、类型、分类、地点、状态进行多条件筛选;
  • 点击卡片进入详情页,查看描述、地点、图片和联系方式;
  • 发布者可以在“我的发布”中标记为已找到或已归还;
  • 所有操作重新写入 localStorage,其他页面重新读取后自动显示最新结果。

2.2 关键流程图与数据流图

核心业务流程图:

flowchart TD A[打开小程序] --> B[首页] B --> C{选择操作} C -->|浏览| D[浏览信息列表] C -->|搜索| E[输入关键词/筛选条件] C -->|发布| F[填写发布表单] D --> G[点击信息卡片] E --> H[查看搜索结果] H --> G G --> I[查看信息详情] I --> J{是否联系发布者} J -->|是| K[联系发布者页] K --> L[一键复制联系方式] J -->|否| M[返回列表] F --> N[上传图片/校验表单] N --> O{校验是否通过} O -->|否| F O -->|是| P[发布成功页] P --> I P --> B

数据流图:

flowchart LR U[用户操作] --> P[页面脚本] P --> D[data.js 数据层] D --> L[(localStorage)] L --> D D --> P P --> R[页面重新渲染] R --> U

我的发布 + 状态更新流程:

flowchart TD A[底部导航-我的] --> B[我的发布页] B --> C{切换标签} C -->|进行中| D[显示进行中信息] C -->|已完成| E[显示已完成信息] D --> F[点击标记为已找到/已归还] F --> G{发布者归属校验} G -->|通过| H[更新状态为 resolved] G -->|不通过| I[提示无权限] H --> J[状态已更新页] J --> B E --> K[查看详情]

搜索筛选流程:

flowchart TD A[搜索页] --> B[输入关键词] B --> C[选择信息类型] C --> D[选择物品分类] D --> E[选择信息状态] E --> F[点击查看搜索结果] F --> G[结果页按 AND 组合过滤] G --> H{是否有结果} H -->|有| I[显示结果列表] H -->|无| J[显示空状态提示] I --> K[点击卡片进详情]

2.3 重要代码片段

片段一:数据层封装(js/data.js)

function addItem(item) {
  const required = ['type', 'name', 'location', 'date', 'contact'];
  for (const field of required) {
    if (!item[field] || String(item[field]).trim() === '') {
      throw new Error(`缺少必填字段:${field}`);
    }
  }
  if (item.type !== 'lost' && item.type !== 'found') {
    throw new Error('type 必须是 lost 或 found');
  }
  const newItem = {
    id: generateId(),
    type: item.type,
    name: item.name.trim(),
    category: item.category || '其他',
    location: item.location.trim(),
    date: item.date,
    description: (item.description || '').trim(),
    contact: item.contact.trim(),
    status: 'active',
    ownerId: getOwnerId(),
    createdAt: Date.now()
  };
  const items = getAllItems();
  items.unshift(newItem);
  saveAllItems(items);
  return newItem;
}

这段代码把“添加一条信息”的所有逻辑集中在一个函数里:校验必填字段、生成唯一 id、记录发布者、设置默认状态、写入 localStorage。页面调用时只需传入一个对象,不需要关心存储细节。

片段二:状态文案统一(js/data.js)

function getStatusText(type, status) {
  if (type !== 'lost' && type !== 'found') return '状态未知';
  if (status !== 'active' && status !== 'resolved') return '状态未知';

  const labels = {
    lost: { active: '寻找中', resolved: '已找到' },
    found: { active: '待认领', resolved: '已归还' }
  };
  return labels[type][status];
}

存储层保持 active / resolved 两种状态,展示层根据信息类型分别显示“寻找中/已找到”和“待认领/已归还”,避免状态语义混乱。

片段三:搜索功能(js/data.js)

function searchItems(keyword) {
  if (!keyword || keyword.trim() === '') return getAllItems();
  const kw = keyword.trim().toLowerCase();
  return getAllItems().filter(item => {
    const name = (item.name || '').toLowerCase();
    const desc = (item.description || '').toLowerCase();
    return name.includes(kw) || desc.includes(kw);
  });
}

搜索同时匹配名称和描述,并统一转小写,实现不区分大小写。

三、附加特点设计与展示

3.1 一键复制联系方式

设计意义:校园失物场景中,用户需要把 QQ 号发给对方,手动输入容易出错。一键复制减少操作步骤。

实现思路:优先使用 navigator.clipboard.writeText,不支持时降级为创建临时 textarea 并 document.execCommand('copy')。并且检查 execCommand 的返回值,失败时如实提示,不会“假成功”。

function fallbackCopy(text) {
  const textarea = document.createElement('textarea');
  textarea.value = text;
  textarea.style.position = 'fixed';
  textarea.style.opacity = '0';
  document.body.appendChild(textarea);
  textarea.select();

  let ok = false;
  try {
    ok = document.execCommand('copy');
  } catch (e) {
    ok = false;
  }

  document.body.removeChild(textarea);
  showToast(ok ? '已复制:' + text : '复制失败,请手动复制');
}

3.2 搜索历史记录

设计意义:用户搜索“校园卡”后,下次想再搜同样关键词,不需要重新输入。

实现思路:每次搜索时把关键词存入 localStorage,去重后最多保留 5 条。渲染历史时使用 data-index 属性配合事件监听,而不是内联 onclick 拼字符串,避免 O'Reilly 这类带单引号的关键词破坏 HTML 属性。

listEl.innerHTML = history.map((kw, index) => `
  <div class="history-item" data-index="${index}">
    <span class="icon">🕐</span>
    ${escapeHtml(kw)}
  </div>
`).join('');

listEl.querySelectorAll('.history-item').forEach(el => {
  el.addEventListener('click', function () {
    const idx = parseInt(this.getAttribute('data-index'), 10);
    const list = getHistory();
    if (list[idx]) {
      useHistory(list[idx]);
    }
  });
});

3.3 状态闭环展示

设计意义:发布者标记“已找到/已归还”后,其他用户应该能立刻看到该信息已解决,避免无效联系。

实现思路:首页、搜索结果页、详情页都调用 getStatusText(item.type, item.status) 获取统一文案;详情页在 resolved 状态下把联系按钮变为禁用的“该信息已解决”。

3.4 我的发布按发布者区分

设计意义:避免一个人修改别人发布的信息,保证数据归属清晰。

实现思路:addItem 时通过 getOwnerId() 生成并记录当前浏览器的用户标识,my-posts.js 只显示 ownerId 与当前用户匹配的记录。

function getOwnerId() {
  let id = localStorage.getItem(OWNER_KEY);
  if (!id) {
    id = 'owner_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
    localStorage.setItem(OWNER_KEY, id);
  }
  return id;
}

3.5 分类精确筛选 + 状态筛选

设计意义:首页分类入口改为按 category 字段精确过滤,搜索结果页新增状态筛选,用户能更快定位想要的信息。

实现思路:首页分类入口跳转 search-result.html?category=校园卡,结果页读取参数后按字段过滤;搜索页新增“全部 / 进行中 / 已解决”筛选按钮,参数通过 URL 传递。

3.6 单张物品图片上传

设计意义:失物招领场景中,图片比文字描述更直观,能帮助失主快速辨认物品。

实现思路:发布页支持选择 JPG/PNG/WebP 图片,用 Canvas 压缩到最长边 1000px,JPEG 质量按 0.78/0.60/0.45 分档,压缩后以 Data URL 形式存入 localStorage。首页、搜索结果页显示缩略图,详情页显示大图;无图片或解码失败时回退到分类图标。

成果展示:

image

3.7 地点关键词筛选 + 组合筛选

设计意义:用户可能只记得“在三区教学楼丢的”,不一定记得物品名称。地点筛选补上了这个场景。

实现思路:搜索页新增地点关键词输入,与物品关键词、类型、分类、状态按 AND 组合筛选,重新搜索时保留条件。

3.8 发布者归属校验

设计意义:避免用户误改他人发布的信息。

实现思路:每条信息记录 ownerId,“我的发布”只显示匹配当前浏览器 ownerId 的记录;无 ownerId 的旧记录保留浏览和搜索,但不自动认领。

四、项目结构与运行说明

4.1 项目目录结构

102401625-102401614/
├── index.html              # 首页:信息浏览、类型筛选、分类快捷入口
├── publish.html            # 发布信息页
├── publish-success.html    # 发布成功页
├── search.html             # 搜索页:关键词、类型/分类/状态筛选、搜索历史
├── search-result.html      # 搜索结果页
├── detail.html             # 信息详情页
├── contact.html            # 联系发布者页:一键复制 QQ
├── my-posts.html           # 我的发布页:进行中/已完成
├── status-updated.html     # 状态更新成功页
├── package.json            # 项目配置与测试脚本
├── README.md               # 目录说明与使用说明
├── css/
│   └── style.css           # 全局样式
├── js/
│   ├── data.js             # 数据层:增删改查、搜索、状态更新、图片处理
│   ├── index.js            # 首页逻辑
│   ├── publish.js          # 发布页逻辑
│   ├── search.js           # 搜索页逻辑
│   ├── detail.js           # 详情页逻辑
│   ├── contact.js          # 联系页逻辑
│   └── my-posts.js         # 我的发布页逻辑
└── tests/
    └── data.test.js        # 单元测试(Mocha + Chai,54 个用例)

目录组织说明:

  • 根目录 HTML 文件对应各个页面,css/ 存放全局样式,js/ 按数据层和页面逻辑拆分;
  • js/data.js 是各页面共用的数据入口,所有增删改查、搜索、状态更新、图片处理都通过它完成;
  • tests/data.test.js 是单元测试文件,按作业要求不提交到仓库,但保留在本地供运行验证。

4.2 项目运行说明

项目使用原生 HTML、CSS 和 JavaScript 编写,没有引入 Vue、React 等第三方前端框架。网页本身不需要启动后端服务器,测试人员下载完整项目后,可以直接用 Chrome 或 Edge 打开 index.html。

运行网页:

  1. 从 GitHub 下载项目到本地;
  2. 用 Chrome 或 Edge 打开 index.html;
  3. 无需安装任何依赖即可运行,localStorage 会自动保存数据。

运行单元测试:

npm ci
npm test

期望输出 54 passing。

注意:如果 PowerShell 提示“禁止运行脚本”,请运行 Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned,或改用 CMD。

4.3 测试人员操作说明

  1. 打开首页,在“最新信息 / 寻物启事 / 招领信息”之间切换,点击分类图标浏览;
  2. 进入搜索页,输入物品名称,组合测试类型、分类、状态筛选;
  3. 打开任意物品详情,检查图片、地点、联系方式、复制按钮;
  4. 点击底部“发布”,选择“寻物/招领”,填写必填项,选择图片后发布;
  5. 进入“我的”,查看发布数量,测试“标记为已找到/已归还”;
  6. 刷新浏览器,确认发布、状态、图片等数据仍然保留。

4.4 本地数据与重置说明

项目数据保存在浏览器当前站点的 localStorage 中,主要包括物品信息、搜索历史等。清除浏览器数据后会丢失。如需恢复初始状态,可打开 Chrome 开发者工具,进入 Application → Local Storage → 清除对应站点数据。

五、单元测试

5.1 测试工具与学习过程

本项目使用 Mocha + Chai。Mocha 是测试框架,负责组织和运行测试用例;Chai 是断言库,提供 expect 语法。选择理由:轻量、配置简单、社区文档丰富,适合本次小型前端项目。

学习过程:阅读 Mocha 官方文档的 “Getting Started” 部分,了解 describe 和 it 的用法;阅读 Chai 的 expect API 文档。遇到的问题是 Node.js 环境没有 localStorage,解决方法是写一个 LocalStorageMock 类模拟浏览器存储行为。

5.2 部分测试代码

it('2. addItem 正常添加一条信息,getAllItems 长度变为 1', function () {
  const item = data.addItem({
    type: 'lost',
    name: '校园卡',
    category: '校园卡',
    location: '三区教学楼',
    date: '2026-10-05',
    description: '蓝色卡套',
    contact: '2766912385'
  });
  expect(item).to.have.property('id');
  expect(item.status).to.equal('active');
  expect(data.getAllItems()).to.have.lengthOf(1);
});

测试的函数是 addItem,验证它返回的对象包含 id、状态默认为 active,并且数据确实写入了存储。

it('13. lost + active 显示“寻找中”', function () {
  expect(data.getStatusText('lost', 'active')).to.equal('寻找中');
});

测试的函数是 getStatusText,验证四种业务状态的文案映射正确。

5.3 测试数据构造思路

  • 正常路径优先:先测“添加成功”“搜索命中”“更新成功”这些主流程。
  • 边界情况:空存储、空关键词、缺少必填字段、非法 type。
  • 异常路径:搜索不存在的关键词、更新不存在的 id、删除不存在的 id。
  • 状态语义覆盖:四种 type/status 组合对应的文案,以及非法 type 和非法 status 的兜底显示。
  • 图片回归用例:无图发布、有图发布、压缩后 Data URL 长度、超容量处理等。
  • 大小写与部分匹配:搜索 airpods 能匹配 AirPods,搜索“深蓝色”能匹配描述。

针对将来测试人员的“刁难”,我们考虑了:传入 null 或 undefined 作为关键词、传入超长字符串、重复添加同一条信息、图片处理失败等场景。

5.4 测试结果

最终测试结果:

image

在原有 43 个测试基础上,新增 11 个图片回归用例。测试范围覆盖数据存储、表单校验、信息发布、搜索筛选、详情查询、我的发布、状态修改、权限判断和异常处理。

六、GitHub 协作与代码签入

6.1 协作方式

项目开发过程中,我们没有两个人直接同时修改 main,而是采用:

最新 main → 创建 feature 分支 → 开发 → Commit → Push → Pull Request → 对方检查 → Merge

林志涛 fork 仓库后提交了两次 Pull Request:

  • PR #1:feat: 完善测试基线并统一失物招领状态,修复 Mocha/Chai 依赖锁文件,统一四种业务状态,新增 8 个状态相关测试,共 20 个测试通过。
    image

  • PR #2:feat: 完善复制与状态校验,支持地点筛选和单张物品图片,新增发布者归属校验、地点筛选、图片上传,测试达到 54 passing。
    image

Commit:

image
image

6.2 遇到的问题与解决方法

问题一:首页缺少状态标签

  • 问题描述:在“我的发布”中把信息标记为已解决后,回到首页,卡片上只显示“寻物/招领”,没有状态标签。
  • 做过哪些尝试:检查 renderList() 函数,发现只渲染了类型标签,没有读取 status 字段。
  • 是否解决:已解决,在卡片模板中通过 getStatusText 增加状态标签。
  • 有何收获:多个页面共用同一份数据时,状态展示要统一检查,不能只改一个页面。

问题二:单元测试 0 passing

  • 问题描述:运行 npm test 显示 0 passing,测试文件没有被执行。
  • 做过哪些尝试:检查 package.json 的 test 脚本,发现 tests/**/*.test.js 在 Windows CMD 下不被正确解析;同时发现 tests/data.test.js 是 0 字节空文件。
  • 是否解决:已解决,把 test 脚本改为 mocha tests/*.test.js,并重新粘贴测试代码。
  • 有何收获:Windows 下的 glob 通配符行为和 Linux 有差异,写 npm script 时要考虑兼容性。

问题三:搜索历史特殊字符 Bug

  • 问题描述:搜索历史里出现 O'Reilly 这种带单引号的内容,点击时可能直接把 JavaScript 弄报错。
  • 做过哪些尝试:检查 renderHistory 函数,发现用内联 onclick 拼关键词,单引号会破坏 HTML 属性。
  • 是否解决:已解决,改用 data-index 属性配合事件监听,关键词只出现在文本内容中。
  • 有何收获:任何时候都不要把用户输入直接拼进 HTML 属性里。

问题四:图片上传的压缩与存储

  • 问题描述:原图可能达到几 MB,直接存入 localStorage 会超容量。
  • 做过哪些尝试:用 Canvas 压缩,按最长边 1000px、JPEG 质量分档处理。
  • 是否解决:已解决,压缩后 Data URL 控制在 524288 字符以内,超容量时提示原因并保留表单。
  • 有何收获:前端存储有容量限制,图片必须先压缩再存。

七、评价队友

7.1 值得学习的地方

朱铮睿在项目整体框架搭建和基础功能实现上完成得比较扎实,为后续功能完善提供了稳定的基础。在开发过程中,他能够根据作业要求及时补充分类筛选、搜索和“我的发布”等功能,并在我提交 PR 后认真检查代码、及时合并修改,使两人的开发进度能够顺利衔接。同时,他对项目整体页面结构和功能流程比较熟悉,在后续调整中也能够较快定位需要修改的位置,这一点值得我学习。

7.2 需要改进的地方

后续如果还有结对编程任务,希望我们可以在正式编码前进一步明确功能划分、数据结构和接口约定,并增加开发过程中的实时沟通。这样可以减少重复修改和后期合并时的协调成本,也能让两个人的开发节奏更加统一。

八、个人总结

林志涛:我在本次作业中主要负责状态语义统一、测试基线修复、地点筛选、图片上传和文档整理。通过这次任务我发现,同一个 resolved 状态在寻物和招领场景下的含义完全不同,寻物叫“已找到”,招领叫“已归还”,如果不在展示层做区分,用户会感到困惑。图片上传这个功能让我第一次认真考虑了前端存储的容量限制——原图几 MB 直接存 localStorage 会爆,必须用 Canvas 压缩到合理尺寸再存,还要处理透明区域、损坏图片、超容量等异常情况。这让我理解了“功能能跑通”和“功能可靠”之间的差距。不足的是我在前端交互上参与较少,后续会加强 JavaScript 的实际练习。

posted @ 2026-10-08 19:33  |llin  阅读(5)  评论(0)    收藏  举报