软件工程第四次作业

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

项目 内容
这个作业属于哪个课程 H202601软件工程与软件工程实践
这个作业要求在哪里 2026秋软件工程结对作业(第二次之程序实现)
这个作业的目标 基于第一次结对作业的原型设计,完成"校园失物招领"核心功能的代码实现
学号姓名 102401337吴昊阳 102401335郭航铭
结对同学的博客链接 吴昊阳 / 郭航铭
GitHub 仓库 https://github.com/HangLog-tech/102401335-102401337
呈现形式 WEB(纯前端,Chrome 直接打开 html 即可运行)

一、结对分工

成员 分工
102401337 吴昊阳 总体架构设计(三层结构 + hash 路由)、纯逻辑层 core.js 与数据层 data.js、单元测试编写、README 与博客撰写、GitHub 仓库维护
102401335 郭航铭 对照第一次 Figma 原型完成界面层 app.js 与样式 style.css 的还原实现、页面走查与交互测试、提出修改意见并 PR 合入

二、PSP 表格

PSP2.1 阶段 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 20 20
Estimate 估计任务总时间 10 10
Analysis 需求分析(含学习新技术) 40 60
Design Spec 生成设计文档 30 40
Design Review 设计复审 20 20
Coding Standard 代码规范制定 10 10
Design 具体设计(路由/数据结构/页面结构) 40 50
Coding 具体编码 240 330
Code Review 代码复审 30 40
Test 测试(自测 + 单元测试 + 修改) 90 150
Reporting 报告 30 60
Test Report 测试报告 20 30
Size Measurement 计算工作量 10 10
Postmortem & Process Improvement Plan 事后总结与改进计划 20 25
合计 610 855

差异分析:编码与测试两项明显超估。编码超时的原因:①首次实践"逻辑/界面分离"结构,core.js 的接口设计返工了一次;②UI 还原阶段为严格对照 Figma 原型调整了多轮样式细节。测试超时的原因:设计双环境(Node + 浏览器)测试方案花了额外时间,但换来助教零配置即可复现测试结果。收获是下一次做预估时,会把"返工缓冲"单独计入。

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

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

本次实现选择 WEB 前端呈现形式。作业要求"下载所有文件后用谷歌浏览器运行 html 文件就能展现预期结果",因此我们做了三个关键决策:

  1. 零依赖、零构建:不使用任何框架和打包工具,纯 HTML/CSS/原生 JavaScript,避免 npm 安装和网络请求带来的复现障碍。
  2. 三层结构,逻辑与界面分离:
    • js/core.js 纯逻辑层:表单校验、关键词匹配、组合筛选、排序、时间格式化、状态流转判断,全部是纯函数,不碰 DOM 和 localStorage——这是可单元测试的基础;
    • js/data.js 数据层:9 条演示种子数据 + localStorage 读写,首次打开自动初始化;
    • js/app.js 界面层:hash 路由 + 六个页面的渲染与事件绑定。
  3. 单页应用 + hash 路由:#/home、#/search、#/publish、#/success/:id、#/detail/:id、#/mine 六个路由,兼容 file:// 协议直接双击打开(ES module 在 file:// 下会被 CORS 拦截,故使用普通 script 标签)。

核心数据流:

用户操作 → app.js 收集表单/筛选条件 → 调用 LAFCore 纯函数(校验/筛选/排序)
        → core.js 返回结果或错误信息 → app.js 渲染列表/卡片/错误提示
        → 写操作经 data.js 存入 localStorage → 下次打开数据仍在

主流程示意(发布 → 浏览/搜索 → 详情 → 联系 → 更新状态):

┌─────────┐   发布    ┌──────────┐  校验通过  ┌──────────┐
│ 发布信息 │ ───────→ │ core.js  │ ────────→ │ localStorage│
│  (表单)  │ ←─报错── │ validate │            └────┬─────┘
└─────────┘          └──────────┘                 │
      ↑                                    首页/搜索列表(倒序)
      │ 去发布                                │
┌─────────┐   点击卡片   ┌──────────┐   一键复制   ┌─────────┐
│ 搜索空态 │ ────────→ │ 物品详情 │ ────────→ │ 联系发布者│
└─────────┘            └────┬─────┘            └─────────┘
                            │ 本人信息
                     ┌──────┴──────┐
                     │ 标记已找到/已归还 │ → 状态 done → 联系方式隐藏
                     │ 删除           │ → 从列表移除
                     └─────────────┘

3.2 关键代码片段及解释

① 表单校验(core.js)——发布信息的守门员

function validateItem(data) {
  var errors = {};
  if (!trim(data.title)) errors.title = '请填写物品名称';
  else if (trim(data.title).length > 20) errors.title = '物品名称不能超过 20 个字';
  if (!data.category || CATEGORIES.indexOf(data.category) === -1)
    errors.category = '请选择物品分类';
  if (!trim(data.time)) errors.time = '请选择' + (data.type === 'found' ? '拾获' : '丢失') + '时间';
  else if (new Date(trim(data.time) + 'T23:59:59').getTime() > Date.now())
    errors.time = '时间不能晚于今天';
  // 地点、联系方式、描述长度校验同理……
  return { ok: Object.keys(errors).length === 0, errors: errors };
}

解释:返回 {ok, errors} 结构而不是直接弹窗,让界面层可以把错误精确挂到对应输入框下方。校验规则含"时间不能晚于今天"这类真实约束。

② 关键词搜索(core.js)——大小写、空白全兼容

function matchKeyword(item, keyword) {
  keyword = normalizeKeyword(keyword);           // trim + 小写 + 空白折叠
  if (!keyword) return true;                     // 空关键词 = 不过滤
  var haystack = [item.title, item.location, item.desc, item.category]
    .map(normalizeKeyword).join(' ');
  return haystack.indexOf(keyword) !== -1;
}

解释:用户输入" 校园卡 "(带空格)与"校园卡"得到相同结果;物品名称、地点、描述、分类四个字段均可被命中,符合"按物品名称等关键词搜索"的要求。

四、附加特点设计与展示

特点 1:一键复制联系方式(含降级方案)

意义:用户看到联系方式后还要长按/选中/复制,步骤越多流失越多。一键复制把"联系发布者"缩短到一次点击。

实现思路:优先使用 navigator.clipboard API;file:// 或旧浏览器环境下自动降级为隐藏 textarea + execCommand('copy'),两种路径都有 toast 反馈。

function copyText(text) {
  if (navigator.clipboard && navigator.clipboard.writeText) {
    navigator.clipboard.writeText(text).then(
      function () { toast('联系方式已复制:' + text); },
      function () { legacyCopy(text); });   // 失败自动降级
  } else { legacyCopy(text); }
}

成果展示:
img

特点 2:完成态隐私保护 + 全场景空状态引导

意义:信息标记"已找到/已归还"后,如果联系方式继续公开展示,发布者会持续收到无效骚扰电话——这正是第一次作业中"减少重复询问和无效联系"痛点的延伸。同时助教点评指出"不能只按一条顺利成功的路线设计产品",我们为首页无数据、搜索无结果、非本人详情三种场景都设计了空状态与下一步引导(如"去发布"按钮)。

实现思路:状态流转集中在 canMarkDone() 一个纯函数判断;渲染层按 status === 'done' 切换联系方式为"已隐藏"。

function canMarkDone(item) {
  return !!item && item.status === 'open';   // 仅进行中可流转,且不可回退
}
// 详情页渲染:(item.status === 'done') ? detailRow('联系方式','已隐藏') : detailRow('联系方式', item.contact)

img

五、目录说明和使用说明

5.1 目录结构

102401335-102401337/
├── index.html          # 入口页面,用浏览器直接打开即可运行
├── css/
│   └── style.css       # 全部样式(严格对照第一次结对作业 Figma 原型的蓝色主题)
├── js/
│   ├── core.js         # 纯逻辑层:表单校验、搜索筛选、排序、时间格式化等(与 DOM 无关,可单元测试)
│   ├── data.js         # 数据层:演示数据(种子数据)+ localStorage 读写
│   └── app.js          # 界面层:hash 路由 + 六个页面的渲染与事件绑定
├── test/
│   ├── core.test.js    # 单元测试用例(共 13 个),Node 与浏览器通用
│   ├── mini-test.js    # 浏览器端零依赖微型测试框架(Node 端用内置 node:test)
│   └── runner.html     # 浏览器测试入口,双击即可查看测试结果
└── README.md           # 本文件

5.2 使用说明(测试人员参照)

运行网页:

  1. 下载本仓库全部文件,保持目录结构不变;
  2. 使用谷歌 Chrome 浏览器直接双击打开 index.html(无需服务器、无需联网、无任何依赖);
  3. 推荐体验路线:首页切换"寻物启事/招领信息"并点卡片进详情 → 顶部搜索框输入"校园卡" → 底部"+"发布一条信息(故意留空可看校验提示)→ "我的发布"里标记完成、删除 → 底部"重置演示数据"恢复初始状态。

运行单元测试(两种方式等价,任选其一):

  • 浏览器方式(零依赖,推荐):Chrome 双击打开 test/runner.html,页面直接显示每个用例通过情况;
  • Node 方式:安装 Node.js 后在项目根目录执行以下任一命令:
node --test test/core.test.js
node --test test/**/*.test.js

六、单元测试

6.1 测试工具选择与学习

我们选用 Node.js 内置的 node:test + node:assert 作为标准测试工具——零依赖、无需 npm install,助教只要有 Node 就能跑。学习路径:先读邹欣老师《关于单元测试和回归测试》理解理念,再参考廖雪峰的 JavaScript 教程和阮一峰峰推荐的 Mocha 实例教程掌握断言写法(assert.strictEqual / assert.deepStrictEqual / assert.ok 的语义与 Mocha 一致)。

考虑到测试人员(助教/同学)的机器不一定装了 Node,我们又写了一个 30 行的微型测试框架 mini-test.js(提供同名的 test 和 assert),同一套测试文件 core.test.js 在浏览器里双击 test/runner.html 也能跑——"每个人都能很容易地运行它"正是单元测试自动化的要求。

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

以下测试的函数:validateItem(表单校验)、filterItems(搜索与组合筛选)、canMarkDone/doneLabel(状态流转):

// 用例4:缺少地点、未来时间、时间格式错误均应报错
test('用例4:缺少地点、未来时间、时间格式错误均应报错', () => {
  const form = validForm();
  form.location = '';
  assert.ok(Core.validateItem(form).errors.location);
  const future = validForm();
  future.time = '2999-01-01';                    // 刁难:穿越时间
  assert.ok(Core.validateItem(future).errors.time);
  const badFormat = validForm();
  badFormat.time = '2026/09/28';                 // 刁难:错误格式
  assert.ok(Core.validateItem(badFormat).errors.time);
});

// 用例9:类型 + 分类 + 地点组合筛选应同时生效
test('用例9:类型 + 分类 + 地点组合筛选应同时生效', () => {
  const r = Core.filterItems(catalog, { type: 'found', category: '电子数码' });
  assert.deepStrictEqual(r.map(i => i.id), ['c']);
});

// 用例13:状态流转边界
test('用例13:寻物完成叫"已找到",招领完成叫"已归还",完成后不可再流转', () => {
  assert.strictEqual(Core.doneLabel('lost'), '已找到');
  assert.strictEqual(Core.canMarkDone({ status: 'open' }), true);
  assert.strictEqual(Core.canMarkDone({ status: 'done' }), false);
  assert.strictEqual(Core.canMarkDone(null), false);   // 刁难:空对象
});

6.3 测试数据构造思路

  • 基线 + 单变量变异(白盒):先构造一个全合法的 validForm(),每个用例只改坏一个字段,确保校验函数的每个分支都被覆盖(空值、超长、非法分类、未来时间、错误格式、过短联系方式);
  • 目录数据覆盖组合:catalog 覆盖寻物/招领 × 4 个分类 × 3 个地点,验证组合筛选的"与"语义,以及空关键词返回全集;
  • 固定时间戳:相对时间用 now 参数注入固定值,避免"测试在午夜跑挂"的不稳定性;
  • 预判刁难问题:关键词带前后空格、大小写混合、不存在的物品(空结果)、null 入参、已完成状态重复标记——这些情况全部有用例兜底。

自评:13 个用例覆盖了纯逻辑层的全部公开函数和主要边界,对当前"无后端"架构是充分的;若将来接入后端,还需补充接口层测试与端到端测试。

6.4 测试结果

img

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

问题 尝试 结果 收获
双击打开 html 时页面空白(file:// 下 ES module 被 CORS 拦截) ①起本地服务器(违背"双击就能跑"要求)②改用普通 script 标签 + 全局命名空间 window.LAFCore 方案②解决 明白了"可复现性"是交付要求的一部分,技术选型要先看运行环境
本机没有安装 Node.js,单元测试无法本地运行验证 ①安装 Node(机器权限受限)②自写 30 行浏览器测试框架,同一测试文件双环境跑 方案②解决,且方便了所有测试人员 测试工具的价值在于"人人都能跑",零依赖是最大善意
一次测试中"搜索校园卡"返回了全部 4 条而非 1 条 逐用例排查,发现是测试夹具 makeItem 的默认描述"蓝色卡套"被所有条目继承,导致关键词误命中 给夹具数据设置差异化描述后通过 缺陷出在测试数据而非被测代码——写夹具时默认值要格外小心
上传照片后刷新页面数据偶发丢失 定位到 localStorage 约 5MB 配额限制,大图写入失败 上传时限制单张 1.5MB 并提示 对浏览器存储能力边界有了量化认识

八、GitHub代码嵌入记录

img

九、评价队友

吴昊阳评价郭航铭:值得学习的地方——对原型还原度要求极高,连胶囊按钮、状态药丸的圆角像素都对照 Figma 反复调整,保证了产品与原型的一致性;走查很仔细,提的交互问题(如"我的发布"里卡片整区可点导致误触按钮)都一针见血。需要改进的地方——习惯于把改动攒到最后一起提交,中间进度不容易被看到,建议按功能小步签入。

郭航铭评价吴昊阳:值得学习的地方——架构思维清晰,"逻辑与界面分离"的拆分让单元测试成为可能,也让我们在分工时几乎零冲突;写 README 和测试时始终站在"测试人员能不能复现"的角度考虑。需要改进的地方——对样式细节的关注度不如逻辑,初期版本 UI 与原型偏差较大,后续沟通成本偏高,建议动手前先对齐视觉稿。

十、个人总结

102401335郭航铭:我负责这次作业的界面还原。真正动手才发现,原型图上看着简单的卡片,落地时要考虑自适应、长文本截断、点击区域、空状态等大量细节;也第一次体会到与队友接口约定(core.js 的函数签名)带来的协作顺畅——我只管调函数拿数据,逻辑对不对有测试兜底。不足是对 Git 提交节奏把控不好,攒大提交导致一次合并冲突,以后一定小步签入。

102401337吴昊阳:如果说第一次作业我们站在产品经理的视角"想清楚了产品",这次就是站在程序员的角度"把想清楚的东西做出来"。最大的体会是《构建之法》里"单元测试是程序员的护城河"这句话——把逻辑抽成纯函数之后,13 个测试用例给了我放心重构的底气;而"让助教双击 html 就能跑、双击 runner.html 就能看测试"这种对可复现性的执着,本质上是把用户思维延伸到了交付环节。PSP 也如实记录了编码和测试的超时,下次预估会更从容。

posted on 2026-10-05 18:43  起名字真的好麻烦  阅读(18)  评论(0)    收藏  举报