2026秋软件工程结对作业(二):校园失物招领程序实现
校园失物招领 —— 第二次结对作业(代码实现)
学号:102401317 姓名:罗炜
学号:102401218 姓名:罗堉楠
| 课程 | 软件工程作业 |
|---|---|
| 作业要求 | 2026秋软件工程结对作业(第二次之程序实现) |
| 具体目标 | 1.完成 “寻回・校园失物招领” 的结对程序开发 2.练习 GitHub 版本管理、单元测试、PSP 记录与软件工程文档撰写 3.原型模型应采用专用的原型设计工具实现 |
| 学号 | 102401317 |
本文是软件工程课程第二次结对作业,基于第一次的原型设计,完成「校园失物招领」核心模块的代码实现。第一次作业(需求分析与原型设计)见 上一篇博客。
相关链接:
- 罗炜的博客:power1897
- 罗堉楠的博客:LYnan
- 本作业博客链接:第二次结对作业
- GitHub 项目地址:https://github.com//102401317-102401218
- 在线演示(GitHub Pages):https://power819.github.io/102401317-102401218/
一、具体分工
| 成员 | 学号 | 主要分工 |
|---|---|---|
| 罗炜 | 102401317 | 整体架构设计、核心业务逻辑(core.js)、数据持久化与渲染层、单元测试 |
| 罗堉楠 | 102401218 | 需求梳理、页面交互走查、样例如测试数据构造、博客撰写与排版 |
两人共同完成:功能范围确认、流程图与数据流图设计、代码走查与回归测试。
二、PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 25 |
| · Estimate | 估计这个任务需要多少时间 | 20 | 20 |
| Development | 开发 | 420 | 570 |
| · Analysis | 需求分析(包括学习新技术) | 40 | 60 |
| · Design Spec | 生成设计文档 | 30 | 45 |
| · Design Review | 设计复审 | 20 | 15 |
| · Coding Standard | 代码规范 | 15 | 20 |
| · Design | 具体设计 | 45 | 60 |
| · Coding | 具体编码 | 150 | 210 |
| · Code Review | 代码复审 | 30 | 40 |
| · Test | 测试(自我测试,修改代码,提交修改) | 90 | 120 |
| Reporting | 报告 | 60 | 75 |
| · Test Report | 测试报告 | 25 | 30 |
| · Size Measurement | 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 25 | 35 |
| 合计 | 530 | 690 |
超时说明:实际比预估多出约 3 小时,主要集中在「具体编码」「测试」和「需求分析」三处——① 编码时为了同时满足「双击 html 就能跑」和「核心逻辑能被 Node 做单元测试」两个要求,调研并实现了 UMD 写法,多花了时间;② 测试阶段用浏览器实测时遇到 file:// 协议下 ES 模块被 CORS 拦截的问题,排查后改用经典 script 标签加载,属于设计阶段没预料到的返工;③ 单元测试从 10 个用例扩展到 30 个,测试数据构造比预想更花心思。
三、解题思路描述与设计实现说明
3.1 代码实现思路(文字描述)
题目要求做一个 WEB 或 APP,让用户「发布寻物/招领 → 浏览或搜索 → 查看详情 → 联系发布者 → 更新状态」。结合第一次的原型,我们选择 WEB 纯前端方案,理由有三:
- 零依赖、易交付:助教要求「下载所有文件后用 Chrome 打开 html 就能展现预期结果」,纯静态页面天然满足,不需要装环境、不需要后端服务器。
- 数据可持久化:用浏览器的
localStorage存数据,发布的信息刷新后仍在,既实现了「数据不会丢」,又不用引入数据库。 - 可测试性:把业务规则抽成纯函数放进
core.js,不碰 DOM、不碰存储,因此既能被浏览器加载,又能被 Node 直接require做单元测试。
整个应用按四层组织(对应目录里的四个 JS 文件),数据流是单向的:
用户操作 → app.js(事件编排,持有全局状态)
├─→ core.js(纯业务逻辑:搜索/筛选/状态机/校验)
├─→ store.js(localStorage 读写)
└─→ render.js(生成 HTML,插入 DOM 前做转义)
这样做的最大好处是关注点分离:改界面不碰逻辑,改逻辑不影响界面,core.js 里的规则可以被 Mocha 单独验证。下面用一张数据流图说明。
3.2 关键流程与数据流图
数据流图(分层):
┌────────────┐ 事件 ┌────────────┐ 调用 ┌──────────────────────┐
│ 用户操作 │ ────────→ │ app.js │ ───────→ │ core.js 纯业务逻辑 │
│ 点击/输入 │ │ (状态+编排) │ │ 搜索/筛选/状态机/校验 │
└────────────┘ └─────┬──────┘ └──────────────────────┘
│
┌──────────┴──────────┐
│ │
┌─────▼──────┐ ┌──────▼─────┐
│ store.js │ │ render.js │
│ localStorage│ │ DOM 渲染 │
│ 读写 │ │ (HTML转义) │
└─────┬──────┘ └──────┬─────┘
│ 持久化 │ 写回
┌─────▼──────┐ ┌──────▼─────┐
│ localStorage│ │ 界面更新 │
└────────────┘ └────────────┘
核心业务流程图:
打开应用 → store.load 读 localStorage
│
├─ 为空 → 写入 8 条种子数据(data.js)
│
▼
渲染首页列表
│
┌─────────┼─────────────┐
│ 浏览/搜索 │ 点击卡片 │ 发布
▼ ▼ ▼
按类型/类别/ 详情页 发布页填表单
关键词筛选 │ │
│ ┌────┴─────┐ 必填项校验
│ │联系发布者 │ ├─ 不通过 → 提示缺失项
│ │(弹窗+复制)│ └─ 通过 → store.add 置顶
│ └────┴─────┘ │
│ │ 标记已找到/已归还 │
│ ▼ ▼
└──────→ 重新渲染(首页/详情/我的 三处同步刷新)
更规范的 Mermaid 版本见仓库
docs/数据流图.md。
状态机(谁修改、从哪入口、改完在哪看到):
发布「招领」 → 待认领 ──发布者标记──→ 已归还
发布「寻物」 → 寻找中 ──发布者标记──→ 已找到
- 谁修改:只有发布者本人操作(在无登录的纯前端里,用
mine字段标记"我发布的",「我的发布」页集中管理); - 入口:详情页底部的「✅ 标记已归还/已找到」按钮,或「我的发布」列表里的按钮;
- 结果:标记后首页、详情页、「我的发布」三处状态标签与统计实时刷新,已完成信息不再显示"标记"按钮。
3.3 重要代码片段及解释
① 核心逻辑用 UMD 包装,浏览器和 Node 都能用(这是"可测试"的关键)
(function (root, factory) {
if (typeof module === 'object' && module.exports) {
module.exports = factory(); // Node 环境:require 拿到对象
} else {
root.Core = factory(); // 浏览器环境:挂到 window
}
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
// ... 所有纯函数 ...
return { searchItems, filterByType, markDoneStatus, validateItem, /* ... */ };
});
② 状态机转移(业务规则的核心)
function markDoneStatus(item) {
if (!item || !isActive(item)) return null; // 已完成/非法 → 不允许转移
return doneStatus(item.type); // found→已归还, lost→已找到
}
③ 关键词搜索(多字段、不区分大小写)
function searchItems(items, keyword) {
var kw = String(keyword == null ? '' : keyword).trim().toLowerCase();
if (!kw) return items.slice();
return items.filter(function (it) {
return [it.name, it.cat, it.loc, it.desc].some(function (f) {
return f && String(f).toLowerCase().indexOf(kw) !== -1;
});
});
}
④ 表单校验(返回结构化错误,便于单元测试)
function validateItem(fields) {
var errors = [];
if (!String(fields.name || '').trim()) errors.push('请填写物品名称');
if (!String(fields.loc || '').trim()) errors.push('请填写地点');
if (!String(fields.contact || '').trim()) errors.push('请填写联系方式');
return { valid: errors.length === 0, errors: errors };
}
⑤ 防 XSS:用户输入插入 DOM 前统一转义
function escapeHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
四、附加特点设计与展示
4.1 设计创意与意义
在满足题目核心功能之外,我们从"用户真实使用"的角度加了几个体验向的小设计:
| 特点 | 意义 |
|---|---|
| 一键复制联系方式 | 失主看到信息后不用手抄号码,点一下复制,去加微信/打电话,缩短「看到 → 联系」的路径 |
| 按类别筛选(7 类) | 招领/寻物之外,还能按"电子设备/证件卡片…"快速缩小范围,比纯关键词搜索更快 |
| 搜索历史 + 热门搜索 | 记住搜过的词,减少重复输入;热门词引导新用户 |
| 数据本地持久化 | 发布的信息刷新不丢,符合"平台"而非"一次性演示"的定位 |
| 相对时间显示 | 显示"2 小时前/3 天前"而不是冷冰冰的日期,一眼判断信息新旧 |
| 防 XSS 转义 | 用户输入的名称/描述/联系方式在渲染前转义,避免注入恶意脚本 |
4.2 实现思路
- 一键复制:优先用
navigator.clipboard.writeText,失败(如file://非安全上下文)时回退到隐藏textarea + execCommand('copy'),保证双击打开也能复制。 - 类别筛选:在首页「全部/招领/寻物」类型 tab 之下,再加一行横向滚动的类别 chips,与类型筛选叠加过滤。
- 搜索历史:搜索词去重后
unshift进数组,最多存 8 条,写入 localStorage;清空按钮一键移除。 - 相对时间:
relativeTime(iso, now)按分钟/小时/天分档,now可注入便于单元测试。
4.3 关键代码片段(一键复制)
function copyText(text) {
function fallback() {
var ta = document.createElement('textarea');
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta); ta.select();
var ok = false;
try { ok = document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
return ok;
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(
function () { toast('✅', '联系方式已复制'); },
function () { fallback() ? toast('✅', '联系方式已复制') : toast('⚠️', '复制失败,请手动复制'); }
);
} else {
fallback() ? toast('✅', '联系方式已复制') : toast('⚠️', '复制失败,请手动复制');
}
}
4.4 实现成果展示
- 在线演示: https://power819.github.io/102401317-102401218/
- 界面截图(首页 / 详情 / 发布 / 我的发布):
1.首页截图

2.详情截图

3.发布截图

4.我的发布截图

五、目录说明与使用说明
5.1 目录结构
102401317-102401218/
├── index.html # 入口页面(5 个页面 + 底部导航 + 弹窗)
├── css/
│ └── style.css # 全部样式(蓝灰配色,主色 #2563EB)
├── js/
│ ├── core.js # 核心纯逻辑:搜索/筛选/状态机/校验/格式化(可被 Node require 做测试)
│ ├── data.js # 8 条内置样例数据
│ ├── store.js # localStorage 读写封装
│ ├── render.js # DOM 渲染层(含 HTML 转义防 XSS)
│ └── app.js # 应用入口:状态管理、事件绑定、页面路由
├── docs/
│ └── 数据流图.md # 关键流程与数据流图(Mermaid)
└── README.md # 目录说明 + 使用说明
分层说明:core.js(业务规则,无 DOM)→ store.js(持久化)→ render.js(界面)→ app.js(编排)。核心逻辑与界面分离,core.js 可被 Node 直接 require 做单元测试。
5.2 测试人员如何运行
- 下载 /
git clone本仓库全部文件到本地; - 用谷歌浏览器(Chrome)双击打开
index.html; - 即可看到界面,无需安装环境、无需联网、无需后端(数据存浏览器 localStorage)。
核心操作:首页浏览 → 点卡片看详情 → 「📞 联系发布者」弹窗(可一键复制)→ 「✅ 标记已归还/已找到」;发布走底部「➕ 发布」填表;「👤 我的」看自己发布的信息和统计。
六、单元测试
6.1 测试工具与简易教程
我们选用 Mocha(作业附录推荐)+ Node 内置的 assert 断言,好处是零额外依赖、一条命令就能跑。
极简教程(三步):
- 安装 Node.js(官网下载 LTS 版,一路下一步);
- 在项目
test/目录下执行npm install(会按package.json装好 mocha); - 执行
npm test,看到绿勾即测试通过。
cd test
npm install
npm test
选择 Mocha 的过程:先看了附录推荐的廖雪峰 JS 教程和《Mocha 实例教程》,Mocha 的
describe/it结构直观、报错信息清晰,配合assert不需要再学断言库,所以选了它。核心逻辑因抽成了纯函数,只需require('../js/core.js')就能测,无需模拟浏览器 DOM。
6.2 测试代码与说明
以下展示部分用例(完整 30 个用例见仓库 test/core.test.js,本地运行):
const assert = require('assert');
const Core = require('../js/core.js');
describe('searchItems 关键词搜索', function () {
it('按物品名称命中', function () {
const r = Core.searchItems(fixture(), '保温杯');
assert.strictEqual(r.length, 1);
assert.strictEqual(r[0].name, '黑色保温杯');
});
it('不区分大小写', function () {
assert.strictEqual(Core.searchItems(fixture(), 'airpods').length, 1);
});
});
describe('markDoneStatus 状态机', function () {
it('招领 待认领 → 已归还', function () {
assert.strictEqual(Core.markDoneStatus({ type: 'found', status: '待认领' }), '已归还');
});
it('已完成信息不可再转移(返回 null)', function () {
assert.strictEqual(Core.markDoneStatus({ type: 'found', status: '已归还' }), null);
});
});
describe('validateItem 表单校验', function () {
it('缺少地点时报错', function () {
const r = Core.validateItem({ name: '保温杯', loc: '', contact: '138' });
assert.strictEqual(r.valid, false);
assert.ok(r.errors.some(e => e.indexOf('地点') !== -1));
});
});
被测函数覆盖:searchItems(搜索)、filterByType/filterByStatus(筛选)、markDoneStatus(状态机)、validateItem(校验)、formatCode(编号)、catEmoji(图标映射)、relativeTime(相对时间)、calcStats(统计)、sortNewest(排序)。测试结果 30 个用例全部通过:
30 passing (4ms)
6.3 构造测试数据的思路
我们主要用白盒方法设计用例,覆盖每个函数的正常、边界与异常输入:
- 正常情况:每个函数的典型输入(如搜索能命中、状态机能正常转移、表单填全能通过);
- 边界情况:空关键词(应返回全部)、空数组(统计为 0)、临界时间(一分钟内显示"刚刚")、编号补零到 6 位;
- 异常情况:非法状态转移(已完成再标记→返回 null)、
null/非数组输入(返回空数组)、非法时间字符串(返回空串)。
对付测试人员刁难:测试人员可能会输入空串、超长文本、中英文混排、重复标记已完成项。所以校验函数用 trim 后判空、搜索统一 toLowerCase 处理中英文、状态机对"已完成再标记"做了防御返回 null。构造的小型 fixture(4 条数据覆盖 found/lost、active/done 各一种)让每个断言都可预期、可复现。
七、GitHub 代码签入记录
我们按功能划分、每完成一个模块 commit 一次;做完后又经过浏览器实测,陆续修复了几个问题,并让罗堉楠通过 fork → pull request 的方式协作。完整签入记录如下(power819 为罗炜账号,LYnan 为罗堉楠账号):
b85cc5f 10-07 10:37 罗炜 fix: 底部导航被挤出可视区(.page 改为 flex:1,所有页面常驻显示)
fae648c 10-07 10:29 罗炜 refactor: 底部导航「我的」改为「我的发布」,与页面标题及作业术语一致
70dd5a6 10-07 10:09 power819 Merge pull request #1 from John-ship-it5552/main
27a35b8 10-07 10:04 LYnan Update README.md
d984738 10-07 08:59 罗炜 fix: 统一无操作按钮的提示文案(data-toast 替代 data-noop)
48741e5 10-07 08:51 罗炜 docs: README 目录说明与使用说明 + 数据流图
0bdd3d2 10-07 08:51 罗炜 feat: 渲染层与交互入口(render.js + app.js)
3f16f6b 10-07 08:51 罗炜 feat: 样例数据与数据持久化(data.js + store.js)
ffa7b3a 10-07 08:51 罗炜 feat: 核心业务逻辑 core.js(搜索/筛选/状态机/校验/格式化)
401964e 10-07 08:51 罗炜 feat: 项目骨架与样式(5 页面 HTML + 蓝灰配色 CSS)
截图如下

八、遇到的异常/困难及解决方法
| 问题 | 尝试 | 是否解决 | 收获 |
|---|---|---|---|
file:// 下 ES 模块被 CORS 拦截:用 <script type="module"> 时,双击打开会白屏报错 |
先以为是代码 bug,逐行排查后发现是 Chrome 对 file:// 协议的模块加载限制;改用经典 <script> 标签按序加载 |
✅ 已解决 | 交付给"双击 html 就能跑"的作业时,要避开 ES 模块;UMD 写法能同时满足浏览器与 Node 两套环境 |
| 核心逻辑无法直接测试:一开始逻辑和 DOM 混在一起 | 把业务规则抽成纯函数放到 core.js,渲染层只做 HTML 生成 |
✅ 已解决 | 纯函数 + 关注点分离,是"可测试"的前提 |
| 状态机边界:已完成信息还能被再次标记 | 给 markDoneStatus 加 isActive 防御,非法转移返回 null,前端按钮同步隐藏 |
✅ 已解决 | 状态转移一定要想清"谁能改、从哪改、改完在哪看到" |
| 浏览器缓存导致调试时看到旧代码 | 清缓存 / 强刷后才看到最新改动 | ✅ 已解决 | 调试网页要养成 Ctrl+F5 强刷的习惯 |
| 中文 Windows 用户名路径 | 部分工具在中文路径下异常,改为用相对路径/英文临时目录 | ✅ 已解决 | 遇到无输出崩溃先怀疑路径编码 |
九、评价队友
罗炜值得学习的地方:承担项目整体架构、核心业务代码、数据持久化、渲染层以及单元测试工作,能把握项目底层骨架,具备较强的代码设计与逻辑实现能力,兼顾数据存储、页面渲染和质量验证,保障项目基础功能稳定落地,是项目核心技术支撑。
罗炜需要改进的地方:更多聚焦技术开发,在需求梳理、交互体验检查方面参与较少,容易出现代码实现和原始需求、用户交互预期存在偏差。后续可提前参与需求讨论,配合做简单交互走查,更好对齐业务预期,减少后期调整成本。。
我对这次结对的反思:在梳理需求阶段,是否做到和罗炜充分沟通架构实现可行性,避免需求描述脱离技术实现方案;交互走查时,要提前预判实现难点,不能只从使用视角检查;构造测试数据要覆盖边界场景,不能仅准备常规样例;同时需及时同步需求变更,减少核心开发人员返工,博客内容也应结合技术实现细节,不只停留在表面效果描述。
附:本文目录说明、使用说明与 README 一致,测试代码因作业要求「单元测试不必上传」仅保留在本地
test/,博客中已完整展示核心用例。

浙公网安备 33010602011771号