校园失物招领小程序 · 结对作业
软件工程 · 需求分析与原型设计

软工第二次结对作业

2026秋软件工程个人作业(第四次)——校园失物招领(结对编程)

项目 内容
这个作业属于哪个课程 2026秋软件工程与软件工程实践
这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/2026-01SoftwareEngineeringandSoftwareEngineeringPractice/homework/16745
结对成员 学号102401635(姓名 尼玛顿珠)、学号 102401623(姓名 杜佳伟)
本作业博客 https://www.cnblogs.com/kwndl/p/23240220
结对同学博客 https://i.cnblogs.com/posts/edit;postId=23241986
GitHub 仓库 https://github.com/kwndl108/102401623-102401635

一、具体分工

  • 【尼玛顿珠】:需求梳理与整体架构设计、数据层(store.js)与工具层(utils.js)实现、单元测试编写、博客撰写;
  • 【杜佳伟】:视图层(app.js)页面渲染与交互实现、界面样式(style.css)、README 编写、GitHub 仓库搭建与合并。

两人先共同讨论功能划分与数据结构,再各自负责独立模块,最后互相 review 代码并一起联调。

二、PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
Estimate 估计这个任务需要多少时间 60 50
Development 开发
Analysis 需求分析(包括学习新技术) 120 110
Design Spec 生成设计文档 60 55
Design Review 设计复审(和同伴一起审核设计文档) 30 25
Coding Standard 代码规范(制定合适的规范) 30 20
Design 具体设计(页面结构、数据结构、函数划分) 90 85
Coding 具体编码 300 290
Code Review 代码复审 60 45
Test 测试(自我测试、修改代码、提交修改) 120 150
Reporting 报告
Test Report 测试报告(单元测试用例设计与运行) 60 45
Size Measurement 计算工作量 15 10
Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 30 25
合计 975 910

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

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

本次在第一次结对作业原型的基础上,用原生 HTML + CSS + JavaScript 实现了完整的 WEB 版「校园失物招领」,不依赖任何框架和构建工具,下载后用谷歌浏览器直接打开 index.html 即可运行。

整体按「数据层 → 工具层 → 视图层」三层组织:

  1. 数据层(store.js):物品数据结构为 {id, type, name, category, place, placeDetail, time, desc, images, contact, status, views, contacts, createdAt, publisher}。所有数据保存在 localStorage(首次运行自动写入 6 条种子数据),提供 add / update / remove / getById / getMine 等 CRUD 接口,update 只修改传入的字段,方便状态切换。
  2. 工具层(utils.js):把搜索过滤、表单校验、时间格式化、状态文案、一键复制、图片压缩等纯逻辑抽成独立函数,不依赖任何 DOM,因此可以脱离页面直接用单元测试框架测试。
  3. 视图层(app.js):用 hash 路由实现五个页面(首页/搜索/发布/详情/我的)的单页切换,页面渲染采用模板字符串,事件统一用 addEventListener 绑定。

核心流程闭环为:发布信息 → 首页/搜索列表展示 → 查看详情 → 联系发布者 → 在「我的」标记已找到/已归还 → 状态全局更新。

3.2 关键实现的数据流图

![校园失物招领小程序 · 程序数据流图]14-数据流图-程序数据流

数据流说明:

  • 发布流程:发布页收集表单 → validatePublish 校验必填项 → 通过后 Store.add 写入 localStorage → 首页与搜索列表即时刷新;
  • 浏览/搜索流程:首页列表或搜索页关键词 → filterItems 组合过滤(关键词+类型+类别+地点+状态)→ 渲染列表 → 点击卡片进入详情(浏览量 +1);
  • 联系流程:详情页点击「联系发布者」→ 联系人次数 +1 → 显示联系方式 → copyText 一键复制到剪贴板;
  • 状态维护流程:「我的发布」点击「标记已找到/已归还」→ Store.update(status) 写入存储 → 全站该物品状态文案同步变化,并支持重新开放。

3.3 重要代码片段

① 搜索过滤函数 filterItems(核心业务逻辑)

filterItems: function (items, cond) {
  cond = cond || {};
  var kw = Utils.normalizeKeyword(cond.keyword);
  return items.filter(function (it) {
    if (cond.type && it.type !== cond.type) return false;
    if (cond.category && it.category !== cond.category) return false;
    if (cond.place && it.place !== cond.place) return false;
    if (cond.status) {
      if (cond.status === 'open' && it.status !== 'open') return false;
      if (cond.status === 'closed' && it.status !== 'closed') return false;
    }
    if (kw) {
      var hay = Utils.normalizeKeyword(
        [it.name, it.desc, it.place, it.placeDetail, it.category].join(' ')
      );
      if (hay.indexOf(kw) === -1) return false;
    }
    return true;
  });
}

设计要点:这是一个纯函数——不修改原数组、不依赖页面状态,输入输出完全确定,所以既能被页面调用,也能被单元测试直接验证。关键词同时匹配名称、描述、地点、类别,并做了大小写与首尾空格归一化,用户搜索" 校园卡 "也能命中。

② 发布表单校验 validatePublish(返回结构化错误)

validatePublish: function (form) {
  var errors = {};
  if (!form.name || !String(form.name).trim()) errors.name = '请填写物品名称';
  if (!form.category) errors.category = '请选择物品类别';
  if (!form.place) errors.place = '请选择丢失/拾取地点';
  if (!form.time || !String(form.time).trim()) errors.time = '请填写丢失/拾取时间';
  if (!form.contact || !String(form.contact).trim()) errors.contact = '请填写联系方式';
  if (String(form.contact || '').trim() && String(form.contact).trim().length < 5) {
    errors.contact = '联系方式过短,请填写完整的微信号或手机号';
  }
  if (form.images && form.images.length > 3) errors.images = '最多上传 3 张图片';
  return Object.keys(errors).length ? { ok: false, errors: errors } : { ok: true, errors: {} };
}

设计要点:校验结果不是简单返回"对/错",而是返回字段级错误对象,视图层拿到后可以精确地在对应输入框下标红提示,用户体验比弹窗提示更好。

四、附加特点设计与展示

4.1 创意独到之处与意义

  • 纯前端 + localStorage 方案:无需服务器即可完整演示"发布—浏览—搜索—联系—状态更新"闭环,刷新不丢数据,方便助教测试;后续可平滑替换为后端接口。
  • 图片上传自动压缩:选择手机照片后 canvas 压缩成 base64 再存储,不占服务器空间,也避免 localStorage 容量超限。
  • 计数体系:每条信息记录浏览数与联系数,帮助发布者判断信息热度,也给数据统计页提供真实数据来源。

4.2 实现思路

把「容易测的逻辑」和「难测的 DOM」分离:搜索、校验、格式化等纯函数全部放进 utils.js,页面只负责"收集输入 → 调函数 → 渲染结果"。这样单元测试无需启动浏览器交互即可覆盖核心逻辑,也让代码结构更清晰。

4.3 重要代码片段

一键复制联系方式(带降级方案)

copyText: function (text) {
  return new Promise(function (resolve) {
    if (navigator.clipboard && window.isSecureContext) {
      navigator.clipboard.writeText(text).then(function () { resolve(true); }, function () {
        legacyCopy(text, resolve);  // 降级到 execCommand
      });
    } else {
      legacyCopy(text, resolve);
    }
  });
}

要点:navigator.clipboard 只在 HTTPS/本地安全上下文中可用,file:// 打开时部分浏览器不可用,因此增加了 execCommand('copy') 的隐藏 textarea 兜底方案,保证本地双击打开也能一键复制。

4.4 实现成果展示

程序界面(谷歌浏览器打开 index.html 实测截图):


首页

搜索页

详情页

发布页

我的发布

单元测试

五、目录说明和使用说明

5.1 目录是如何组织的

campus-lost-found/
├── index.html          # 程序入口(谷歌浏览器直接打开)
├── README.md           # 目录说明 + 使用说明
├── css/
│   └── style.css       # 全局样式(绿色主题、卡片布局、响应式)
├── js/
│   ├── store.js        # 数据层:localStorage 读写、CRUD、计数、种子数据
│   ├── utils.js        # 工具层:搜索过滤、校验、格式化、复制、图片压缩(纯函数)
│   └── app.js          # 视图层:hash 路由、5 个页面渲染、弹窗/toast、事件绑定
├── images/             # 物品示例图片(来自第一次作业原型)
└── test/
    ├── test.html       # 单元测试页面(打开即运行)
    └── lib/            # 本地化测试框架 mocha.js / mocha.css / chai.min.js

依赖关系:index.html 按 store.js → utils.js → app.js 顺序加载脚本;测试页只加载 store.js 和 utils.js(纯逻辑,不依赖页面)。

5.2 测试人员如何运行

  1. 从 GitHub 克隆/下载项目全部文件,保持目录结构不变;
  2. 使用谷歌浏览器(Chrome) 双击打开 index.html 即可使用,无需安装任何环境、无需联网;
  3. 运行单元测试:用谷歌浏览器打开 test/test.html,页面自动运行全部测试用例(Mocha/Chai 已放在本地 test/lib/,离线可跑);
  4. 「我的」页面提供"重置数据"按钮,可一键恢复 6 条示例数据,方便反复测试发布/编辑/标记状态等操作。

六、单元测试

6.1 选用的测试工具与简易教程

选用 Mocha + Chai(浏览器版)。Mocha 是 JS 最流行的测试框架,提供 describe / it 描述式用例组织;Chai 提供 assert 断言库。学习途径:Mocha 官方文档 + 阮一峰的 Mocha 入门教程 + 廖雪峰 JavaScript 教程的测试章节。

简易上手步骤:

  1. 下载 mocha.js、mocha.css、chai.min.js 放入 test/lib/(本地化,离线可用);
  2. 在 test.html 中按顺序引入 mocha.css → mocha.js → chai.min.js,并调用 mocha.setup('bdd');
  3. 引入被测的 store.js、utils.js;
  4. 写用例:describe('分组', function(){ it('用例描述', function(){ assert.equal(实际值, 期望值); }); });
  5. 页面底部调用 mocha.run(),浏览器打开即出结果。

6.2 部分单元测试代码与说明

测试对象:搜索过滤、发布校验、状态文案、时间格式化、数据层 CRUD 等纯逻辑。

// 搜索过滤测试使用独立构造的 fixture 数据,
// 不依赖 localStorage 的真实状态(避免手动操作过数据后导致用例失败)
var FIX = [
  { id: 'f1', type: 'lost', name: '校园卡', category: '证件卡片', place: '图书馆', desc: '蓝色校园卡', status: 'open' },
  { id: 'f2', type: 'found', name: '白色无线耳机', category: '数码电子', place: '运动场', desc: '白色 AirPods 风格无线耳机', status: 'open' },
  { id: 'f3', type: 'lost', name: '黑色折叠雨伞', category: '服饰箱包', place: '图书馆', desc: '自动伞', status: 'open' },
  { id: 'f4', type: 'found', name: '蓝色保温杯', category: '杯具水壶', place: '第二食堂', desc: '贴纸款', status: 'closed' }
];

describe('Utils.filterItems(搜索过滤)', function () {

  it('按物品名称关键词过滤:搜索"校园卡"能命中名称中含校园卡的信息', function () {
    var res = Utils.filterItems(FIX, { keyword: '校园卡' });
    assert.ok(res.length >= 1);
    res.forEach(function (it) { assert.match(it.name, /校园卡/); });
  });

  it('关键词忽略大小写与首尾空格:搜索" AirPods"等价于"airpods"', function () {
    var a = Utils.filterItems(FIX, { keyword: '  AirPods  ' });
    var b = Utils.filterItems(FIX, { keyword: 'airpods' });
    assert.deepEqual(a.map(function (x) { return x.id; }), b.map(function (x) { return x.id; }));
  });

  it('组合条件:type=lost 且 关键词=雨伞', function () {
    var res = Utils.filterItems(FIX, { type: 'lost', keyword: '雨伞' });
    assert.equal(res.length, 1);
    assert.equal(res[0].name, '黑色折叠雨伞');
  });

  it('无匹配时返回空数组(助教刁难:搜索不存在的词)', function () {
    var res = Utils.filterItems(FIX, { keyword: '月球车' });
    assert.equal(res.length, 0);
  });
});

上述用例分别测试了:关键词匹配名称、大小写/空格归一化、组合筛选、无结果边界四类情况,覆盖搜索模块的主要分支。

6.3 构造测试数据的思路

  1. 搜索/筛选测试使用独立构造的 fixture 数据:不读取 localStorage 的真实状态,避免用户或测试人员手动操作过数据后(如已标记几条"已找到")导致用例结果不稳定——测试结果与运行环境解耦,任何时候打开都稳定;
  2. 数据层 CRUD 测试使用真实种子数据 + 独立 id:发布/删除用临时 id(t1/t2)操作后立即还原,状态修改用例标记后也还原为初始状态,保证用例之间互不影响;
  3. 覆盖正常/边界/异常三类:正常(合法表单、有关键词命中)、边界(空关键词、空数组、图片恰好 3 张)、异常(非法日期、联系方式 1 位、搜索不存在的词、不存在的 id);
  4. 针对"助教刁难"设计用例:搜索特殊词"月球车"验证空态逻辑、传 null/undefined 验证函数不崩溃、多次 generateId 验证不重复、断言纯函数不修改原数组。

最终共 37 个用例,全部通过(passes: 37, failures: 0)。从覆盖角度看,搜索、校验、状态、时间、CRUD 等核心逻辑的主要分支均已被覆盖,可以满足该程序的测试要求。

七、GitHub 代码签入记录

![GitHub 签入记录截图]image

commit 信息遵循「按功能划分、每完成一个功能提交一次」的约定,例如:

feat: 完成数据层 store.js(CRUD + localStorage 持久化)
feat: 完成首页与信息列表渲染
feat: 完成发布页表单与必填校验
feat: 完成搜索页过滤与空态
feat: 完成详情页联系发布者与一键复制
feat: 完成我的发布(编辑/标记状态/删除)
test: 新增 Mocha 单元测试(37 个用例)
docs: 添加 README 目录说明与使用说明

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

问题描述 做过哪些尝试 是否解决 有何收获
单元测试首次运行时大量用例失败,经排查是测试页没有先初始化种子数据,Store.getAll() 读到空数组 尝试直接运行发现 localStorage 为空;检查后确认 store.js 的初始化逻辑只在页面入口调用,测试页未调用 已解决:在测试脚本开头显式调用 Store.init(),并补了一条"初始化"说明 测试环境与运行环境要分开初始化,被测模块的依赖状态必须显式管理
navigator.clipboard 在 file:// 协议下不可用,复制功能失效 先尝试 document.execCommand('copy') 兜底,又封装成 Promise 统一返回结果 已解决:采用 clipboard 优先 + execCommand 隐藏 textarea 降级的双通道方案 兼容性处理要考虑不同的运行环境,不能只看一种场景
发布页内容较长,底部表单被固定导航栏遮挡 调整页面 padding-bottom 预留空间,给表单区留出足够高度 已解决 固定定位元素要提前预留占位,布局细节需要在真机/浏览器里反复验证
搜索时大小写不敏感但中文无大小写概念,英文关键词匹配不稳定 统一用 toLowerCase() + 首尾空格裁剪后拼接匹配 已解决 搜索匹配要先做输入归一化,再进入匹配逻辑

九、评价你的队友

值得学习的地方:搭档对页面布局和交互细节很敏锐,发布页的校验提示、列表卡片的信息层级都是他坚持打磨的,最终界面比初版整齐很多;遇到 bug 时习惯先复现再定位,思路清晰。

需要改进的地方:有时候会先动手写再回头梳理结构,建议下次先花十分钟把函数边界和数据流画出来再编码;另外 commit 信息偶尔写得比较随意,统一成 feat:/fix:/docs:/test: 前缀后协作更顺畅。

posted @ 2026-10-09 18:39  kwndl  阅读(2)  评论(0)    收藏  举报
● 校园失物招领小程序 · 结对设计作业 | 2026 秋 · 软件工程实践