软件工程第四次作业
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 文件就能展现预期结果",因此我们做了三个关键决策:
- 零依赖、零构建:不使用任何框架和打包工具,纯 HTML/CSS/原生 JavaScript,避免 npm 安装和网络请求带来的复现障碍。
- 三层结构,逻辑与界面分离:
js/core.js纯逻辑层:表单校验、关键词匹配、组合筛选、排序、时间格式化、状态流转判断,全部是纯函数,不碰 DOM 和 localStorage——这是可单元测试的基础;js/data.js数据层:9 条演示种子数据 + localStorage 读写,首次打开自动初始化;js/app.js界面层:hash 路由 + 六个页面的渲染与事件绑定。
- 单页应用 + 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); }
}
成果展示:

特点 2:完成态隐私保护 + 全场景空状态引导
意义:信息标记"已找到/已归还"后,如果联系方式继续公开展示,发布者会持续收到无效骚扰电话——这正是第一次作业中"减少重复询问和无效联系"痛点的延伸。同时助教点评指出"不能只按一条顺利成功的路线设计产品",我们为首页无数据、搜索无结果、非本人详情三种场景都设计了空状态与下一步引导(如"去发布"按钮)。
实现思路:状态流转集中在 canMarkDone() 一个纯函数判断;渲染层按 status === 'done' 切换联系方式为"已隐藏"。
function canMarkDone(item) {
return !!item && item.status === 'open'; // 仅进行中可流转,且不可回退
}
// 详情页渲染:(item.status === 'done') ? detailRow('联系方式','已隐藏') : detailRow('联系方式', item.contact)

五、目录说明和使用说明
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 使用说明(测试人员参照)
运行网页:
- 下载本仓库全部文件,保持目录结构不变;
- 使用谷歌 Chrome 浏览器直接双击打开
index.html(无需服务器、无需联网、无任何依赖); - 推荐体验路线:首页切换"寻物启事/招领信息"并点卡片进详情 → 顶部搜索框输入"校园卡" → 底部"+"发布一条信息(故意留空可看校验提示)→ "我的发布"里标记完成、删除 → 底部"重置演示数据"恢复初始状态。
运行单元测试(两种方式等价,任选其一):
- 浏览器方式(零依赖,推荐):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 测试结果

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

九、评价队友
吴昊阳评价郭航铭:值得学习的地方——对原型还原度要求极高,连胶囊按钮、状态药丸的圆角像素都对照 Figma 反复调整,保证了产品与原型的一致性;走查很仔细,提的交互问题(如"我的发布"里卡片整区可点导致误触按钮)都一针见血。需要改进的地方——习惯于把改动攒到最后一起提交,中间进度不容易被看到,建议按功能小步签入。
郭航铭评价吴昊阳:值得学习的地方——架构思维清晰,"逻辑与界面分离"的拆分让单元测试成为可能,也让我们在分工时几乎零冲突;写 README 和测试时始终站在"测试人员能不能复现"的角度考虑。需要改进的地方——对样式细节的关注度不如逻辑,初期版本 UI 与原型偏差较大,后续沟通成本偏高,建议动手前先对齐视觉稿。
十、个人总结
102401335郭航铭:我负责这次作业的界面还原。真正动手才发现,原型图上看着简单的卡片,落地时要考虑自适应、长文本截断、点击区域、空状态等大量细节;也第一次体会到与队友接口约定(core.js 的函数签名)带来的协作顺畅——我只管调函数拿数据,逻辑对不对有测试兜底。不足是对 Git 提交节奏把控不好,攒大提交导致一次合并冲突,以后一定小步签入。
102401337吴昊阳:如果说第一次作业我们站在产品经理的视角"想清楚了产品",这次就是站在程序员的角度"把想清楚的东西做出来"。最大的体会是《构建之法》里"单元测试是程序员的护城河"这句话——把逻辑抽成纯函数之后,13 个测试用例给了我放心重构的底气;而"让助教双击 html 就能跑、双击 runner.html 就能看测试"这种对可复现性的执着,本质上是把用户思维延伸到了交付环节。PSP 也如实记录了编码和测试的超时,下次预估会更从容。
浙公网安备 33010602011771号