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

校园失物招领 —— 第二次结对作业(代码实现)

学号:102401317 姓名:罗炜
学号:102401218 姓名:罗堉楠

课程 软件工程作业
作业要求 2026秋软件工程结对作业(第二次之程序实现)
具体目标 1.完成 “寻回・校园失物招领” 的结对程序开发
2.练习 GitHub 版本管理、单元测试、PSP 记录与软件工程文档撰写
3.原型模型应采用专用的原型设计工具实现
学号 102401317

本文是软件工程课程第二次结对作业,基于第一次的原型设计,完成「校园失物招领」核心模块的代码实现。第一次作业(需求分析与原型设计)见 上一篇博客。

相关链接:


一、具体分工

成员 学号 主要分工
罗炜 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 纯前端方案,理由有三:

  1. 零依赖、易交付:助教要求「下载所有文件后用 Chrome 打开 html 就能展现预期结果」,纯静态页面天然满足,不需要装环境、不需要后端服务器。
  2. 数据可持久化:用浏览器的 localStorage 存数据,发布的信息刷新后仍在,既实现了「数据不会丢」,又不用引入数据库。
  3. 可测试性:把业务规则抽成纯函数放进 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}

四、附加特点设计与展示

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 实现成果展示

c775d110d03505161a58424ce303b29

2.详情截图

c8bab9e26c855097be5d30a3a5568a9

3.发布截图

image

4.我的发布截图

d9079e63475b6af045fe5b47e9fff60


五、目录说明与使用说明

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 测试人员如何运行

  1. 下载 / git clone 本仓库全部文件到本地;
  2. 用谷歌浏览器(Chrome)双击打开 index.html;
  3. 即可看到界面,无需安装环境、无需联网、无需后端(数据存浏览器 localStorage)。

核心操作:首页浏览 → 点卡片看详情 → 「📞 联系发布者」弹窗(可一键复制)→ 「✅ 标记已归还/已找到」;发布走底部「➕ 发布」填表;「👤 我的」看自己发布的信息和统计。


六、单元测试

6.1 测试工具与简易教程

我们选用 Mocha(作业附录推荐)+ Node 内置的 assert 断言,好处是零额外依赖、一条命令就能跑。

极简教程(三步):

  1. 安装 Node.js(官网下载 LTS 版,一路下一步);
  2. 在项目 test/ 目录下执行 npm install(会按 package.json 装好 mocha);
  3. 执行 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)

截图如下
image


八、遇到的异常/困难及解决方法

问题 尝试 是否解决 收获
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/,博客中已完整展示核心用例。

posted @ 2026-10-09 15:40  LYnan  阅读(7)  评论(0)    收藏  举报