软件工程第二次结对作业

2026秋软件工程第二次结对作业之程序实现 —— 校园失物招领平台

项目 内容
同学 1 马英慧 学号 102401604
同学 2 黄丽婷 学号 102401606
结对同学博客链接 https://www.cnblogs.com/halttt/p/23237145
本作业博客链接 https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16744
仓库地址 https://github.com/halttt/102401604-102401606
呈现形式 WEB 网页(PC 端),统一使用 Chrome 浏览器运行

一、结对信息与项目概况

本项目为「校园失物招领平台」,是在第一次结对作业(原型设计)基础上完成的程序实现。程序围绕「发布信息 — 浏览 / 搜索 — 查看详情 — 联系发布者 — 更新状态」这一基本流程,实现了寻物启事与招领启事的发布、集中浏览、关键词搜索、详情查看与状态维护。

  • 技术栈:原生 HTML + CSS + JavaScript,数据保存在浏览器 localStorage 中,不依赖任何后端服务器、不依赖任何第三方框架或外部资源。
  • 运行方式:下载全部文件后,用 Chrome 双击打开 index.html 即可运行,符合「下载所有文件后双击 html 就能展现预期结果」的要求。
  • 功能范围:严格按作业规定,未涉及复杂后台、实名认证、即时聊天、地图定位,把力气集中在发布 / 搜索 / 详情 / 状态维护这条主链路上。

二、具体分工

成员 学号 主要分工
马英慧 102401604 核心 JS 交互逻辑(localStorage 数据读写、搜索与分类筛选、详情渲染、状态修改、一键复制、我的发布);单元测试用例的设计与实现;GitHub 协作流程(fork → 提交 → Pull Request → 代码复审合并);README 与目录 / 使用说明
队友 102401606 页面布局与 HTML 骨架搭建;CSS 样式设计与美化(卡片、配色、圆角、间距、响应式);整体视觉风格把控;协助功能联调与截图整理

两人在编码过程中互相审阅代码、共同排查问题:一方写完一个功能就提交,另一方在 Pull Request 里做 code review,发现问题及时讨论修改,保证「每人都有清晰、合理的签入记录」。


三、PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 20 25
· Estimate 估计这个任务需要多少时间 20 25
Development 开发 480 620
· Analysis 需求分析(包括学习新技术) 60 50
· Design Spec 生成设计文档 30 25
· Design Review 设计复审 20 15
· Coding Standard 代码规范 15 10
· Design 具体设计 45 40
· Coding 具体编码 240 365
· Code Review 代码复审 30 45
· Test 测试(自我测试、修改代码、提交修改) 40 70
Reporting 报告 95 115
· Test Report 测试报告 30 40
· Size Measurement 计算工作量 15 15
· Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 50 60
合计 595 760

说明:实际编码耗时明显超出预估(365 分钟 vs 预估 240 分钟),主要原因是多人协作时出现了「同一文件被覆盖、内联脚本与外部脚本变量冲突」等返工(详见第九节);测试环节也因反复走查「发布信息不完整、搜索无结果」等异常路径而超时。这些偏差被记录在 PSP 中,用于改进下一次任务的估计。


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

4.1 整体思路

第一次结对作业已经确定了产品方案与页面结构,本次的任务是把它「变成能跑起来的代码」。因此整体思路是:先把数据层定死,再让四个页面对接同一份数据。

由于作业要求「下载文件后用 Chrome 直接打开 html 就能运行」,我们放弃了后端方案,改用浏览器 localStorage 充当数据存储:所有信息以一个 JSON 数组的形式存在 lostFoundList 这个键里,页面之间不通过网络通信,读写统一经由 js/main.js 中的函数完成。这样既满足了「双击即可运行」,又天然实现了「发布后其他页面能看到」的效果。

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

程序共 4 个页面,共用一份 js/main.js 与一份 css/style.css:

  1. 数据层(initData / getList / saveList)
    首次打开时,initData() 会向 localStorage 写入 3 条示例数据(校园卡、钥匙、AirPods),保证页面一打开就有内容可看;getList() 统一负责「读取 → 解析 JSON」,saveList() 统一负责「序列化 → 写回」,所有模块都不直接操作 localStorage,避免读写风格不统一。

  2. 首页浏览 / 搜索 / 筛选(renderList / searchList / switchTab)
    页面加载后调用 renderList(),遍历数据动态生成卡片;搜索框输入关键词后由 searchList() 按「物品名称」模糊匹配;顶部三个标签「全部 / 寻物启事 / 招领启事」由 switchTab() 切换。为避免「搜索」和「筛选」互相打架,用全局变量 currentTab 记录当前所处的标签页,两个操作都先按 currentTab 过滤、再按关键词过滤,保证组合条件正确。

  3. 发布信息(addItem)
    表单提交时先做必填校验(称呼、标题、物品名称、联系方式不能为空),通过后把新记录 push 进数组并写回。新记录的 id 用 Date.now() 生成,天然不重复;同时把用户填的称呼写入 myName,实现「下次发布不用重填」。

  4. 详情查看(loadDetail / copyContact / changeStatus)
    首页卡片点击后跳转 detail.html?id=xx,详情页从 URL 的 query 中取出 id,在数据里查找到对应记录并渲染。发布者可通过「📋 一键复制」把联系方式复制到剪贴板,也可以点「✓ 已找到 / ✓ 已归还」修改状态,changeStatus() 写回后立即重新渲染,状态实时更新。

  5. 我的发布(loadMyPosts)
    按「我的称呼」过滤出本人发布的所有信息,方便统一管理自己发过的内容。

4.3 关键实现的流程图与数据流图

下图为本程序的核心功能流程图(用户视角的操作闭环):

8-核心功能流程图

下图为本程序的数据流图(页面、核心函数与数据存储之间的关系):

9-数据流图

4.4 关键代码片段及解释

(1)统一的数据读写层——把「读 / 写」收口到两个函数,四个页面复用同一套逻辑,避免格式不一致。

// ========== 初始化数据 ==========
function initData(){
    if(!localStorage.getItem("lostFoundList")){
        const initList = [
            { id:1, type:"lost", author:"张三", title:"校园卡丢失",
              itemName:"校园卡", description:"周三下午一食堂三楼丢失,卡面有照片",
              contact:"13800138000", status:"pending", createTime:"2026-09-28" },
            // ...(其余两条示例数据)
        ];
        localStorage.setItem("lostFoundList", JSON.stringify(initList));
    }
}

// ========== 数据读写 ==========
function getList(){
    initData();
    return JSON.parse(localStorage.getItem("lostFoundList"));
}
function saveList(list){
    localStorage.setItem("lostFoundList", JSON.stringify(list));
}

(2)搜索与筛选的组合逻辑——用 currentTab 记录当前标签,保证「先筛选、再搜关键词」,两个功能同时生效不冲突。

let currentTab = 'all';

// 标签切换
function switchTab(tab, btn){
    currentTab = tab;
    document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
    btn.classList.add('active');
    let data = getList();
    if(tab !== 'all') data = data.filter(i=>i.type===tab);
    renderList(data);
}

// 搜索
function searchList(){
    const kw = document.getElementById("searchInput").value.trim().toLowerCase();
    let data = getList();
    if(currentTab !== 'all') data = data.filter(i=>i.type===currentTab);
    if(kw) data = data.filter(i=>i.itemName.toLowerCase().includes(kw));
    renderList(data);
}

(3)状态维护——「物品找回 / 归还」后由发布者标记,写回后立即重渲染,保证其他页面看到的是最新状态。

function changeStatus(id, newStatus){
    const list = getList();
    const target = list.find(x=>x.id===id);
    if(!target) return;
    target.status = newStatus;
    saveList(list);
    alert("状态修改成功!");
    loadDetail();
}

(4)一键复制联系方式——减少手动抄写手机号 / 微信号的麻烦。

function copyContact(text){
    navigator.clipboard.writeText(text).then(()=>alert("联系方式已复制!"));
}

4.5 附加特点设计与展示

除了作业要求的基本功能,我们还做了一些方便用户的小设计:

# 附加特点 说明与意义
1 分类标签筛选 顶部「全部 / 寻物启事 / 招领启事」一键切换,只看自己关心的类型
2 一键复制联系方式 详情页把手机号 / QQ / 微信一键复制到剪贴板,避免手动输入出错
3 称呼自动记忆 发布时填过的称呼存入 myName,下次打开发布页、我的发布页自动带出
4 「我的发布」聚合页 按称呼筛选出本人发布过的全部信息,方便统一管理与修改状态
5 状态可视化管理 每条信息有「待处理 / 已找到 / 已归还」三种状态且配色区分,减少重复询问和无效联系
6 空状态友好提示 无搜索结果、未发布过信息、找不到对应记录时都给出明确文字提示,而不是空白页
7 寻物 / 招领配色区分 卡片左侧色条按类型区分(寻物红、招领绿),一眼分辨信息类型
8 内置单元测试入口 测试代码随程序发布,控制台输入 runAllTest() 即可一键自测,无需安装任何环境

五、单元测试

5.1 测试工具与学习过程(附简易教程)

选用工具:本项目采用原生 JavaScript 断言 + 浏览器控制台输出的轻量测试方案,测试代码直接写在 js/main.js 末尾,通过控制台执行 runAllTest() 触发。

为什么不用 Mocha / Jest?
我们学习了廖雪峰的 JavaScript 教程、阮一峰的博客以及《测试框架 Mocha 实例教程》等资料,理解了单元测试「断言 + 用例组织 + 结果汇总」的基本模型。但考虑到本作业要求「下载文件后用 Chrome 打开 html 就能运行,助教参照目录说明即可复现」,如果再引入 Node、npm、Mocha,会让评分人员多出「装环境」的门槛。因此我们把 Mocha 的核心思想(断言、用例、通过 / 失败计数)用最朴素的原生 JS 实现出来,做到零依赖、双击即跑。

简易教程(自己怎么写一个控制台单元测试):

  1. 写一个「断言函数」:接收实际值与期望值,相等就 pass++,否则 fail++ 并打印失败信息;
  2. 按「一个用例一件事」组织代码,每个用例只验证一个行为,并给它编号(T1、T2…);
  3. 用例之间尽量互不影响,测试产生的脏数据要在结束时清理掉;
  4. 最后统一打印 通过 X,失败 Y,一眼看出结果。

5.2 单元测试代码与说明

以下为项目中的单元测试代码(节选,完整代码见 js/main.js 末尾的 runAllTest()):

function runAllTest(){
    console.log("===== 开始单元测试 =====");
    let pass=0, fail=0;
    const oldLen = getList().length;

    // T1 正常新增一条记录
    let arr = getList();
    arr.push({id:99999,type:"lost",author:"测试",title:"测试水杯",
              itemName:"水杯",description:"测试",contact:"111",
              status:"pending",createTime:getNowDate()});
    saveList(arr);
    getList().length === oldLen+1 ? (console.log("T1 通过:新增记录"),pass++) : (console.log("T1 失败"),fail++);

    // T2 搜索关键词命中物品名称
    getList().some(i=>i.itemName.includes("水杯")) ? (console.log("T2 通过:搜索命中"),pass++) : (console.log("T2 失败"),fail++);

    // T3 搜索不存在的关键词,返回空
    !getList().some(i=>i.itemName.includes("火箭123")) ? (console.log("T3 通过:无结果返回空数组"),pass++) : (console.log("T3 失败"),fail++);

    // T4 修改状态为已找到
    let l = getList(); let it = l.find(x=>x.id===99999);
    it.status = "found"; saveList(l);
    getList().find(x=>x.id===99999).status==="found" ? (console.log("T4 通过:状态改为已找到"),pass++) : (console.log("T4 失败"),fail++);

    // T5 修改状态为已归还
    // ...(同 T4,改为 returned)
    // T6 查询不存在的ID,返回空
    // T7 物品名称为空时拦截提交
    // T8 按类型筛选(寻物 / 招领)
    // T9 我的发布按称呼筛选
    // T10 搜索大小写不敏感

    console.log(`===== 测试结束:通过 ${pass},失败 ${fail} =====`);
    saveList(getList().filter(i=>i.id!==99999));   // 清理测试脏数据
}

各用例测试的函数与验证目标:

用例 测试的函数 / 行为 验证目标
T1 saveList() / getList() 新增一条记录后总数 +1
T2 搜索逻辑(itemName 匹配) 关键词能命中物品名称
T3 搜索逻辑 不存在的关键词返回空数组,不报错
T4 changeStatus('found') 状态能正确改为「已找到」
T5 changeStatus('returned') 状态能正确改为「已归还」
T6 详情查找(find) 不存在的 ID 返回空,页面显示「找不到这条信息」
T7 表单校验 物品名称为空时被拦截,不允许提交
T8 switchTab() 类型筛选 按「寻物 / 招领」筛选结果正确
T9 loadMyPosts() 「我的发布」按称呼筛选结果正确
T10 搜索逻辑 搜索不区分大小写(toLowerCase)

5.3 构造测试数据的思路

我们按「正常值 → 边界值 → 异常值」三类来设计测试数据,尽量覆盖各种情况:

  • 正常值:新增一条合法记录(T1)、搜索一个肯定存在的关键词(T2)、把状态正常改成已找到 / 已归还(T4、T5);
  • 边界值 / 空值:搜索时关键词为空、物品名称为空(T7)、「我的发布」查一个没发过东西的称呼;
  • 异常值:搜索一个绝对不存在的关键词(T3)、查询一个不存在的 ID(T6);
  • 组合与细节:类型筛选与关键词搜索的组合(T8、T9)、大小写不一致的搜索(T10)。

如何应对测试人员的「刁难」? 我们重点想了两类容易被挑刺的场景:一是空数据 / 空输入(比如搜索框什么都不填、直接点搜索),程序应返回全部数据而不是报错;二是无效操作(比如手动改 URL 里的 ?id= 为一个不存在的值),程序应给出「找不到这条信息」的友好提示而不是白屏。测试用例 T3、T6、T7 就是冲着这两类场景设计的。

5.4 测试运行结果

打开 Chrome,按 F12 调出控制台,输入 runAllTest() 回车,结果如下(10 个用例全部通过):
4-单元测试与报错


六、目录说明与使用说明

6.1 目录结构

102401604-102401606/
├─ index.html          # 首页:浏览全部失物招领、关键词搜索、分类筛选
├─ publish.html        # 发布页:填写寻物 / 招领表单
├─ detail.html         # 详情页:查看详细信息、一键复制联系方式、修改状态
├─ myposts.html        # 我的发布:按称呼查询本人发布过的信息
├─ css/
│   └─ style.css       # 全部页面共用的样式
├─ js/
│   └─ main.js         # 全部业务逻辑:本地存储读写、搜索筛选、状态修改、单元测试
└─ README.md           # 目录说明与使用说明

目录组织的原则是「按职责分层」:HTML 只负责页面结构,样式集中在 css/,交互逻辑集中在 js/,四个页面共用同一份 CSS 与 JS,避免重复代码。

6.2 运行与使用说明

运行环境:Windows / macOS 均可,浏览器统一使用 Google Chrome(防止不同浏览器效果不一致)。

  1. 从 GitHub 仓库下载整个项目(Code → Download ZIP)并解压;
  2. 双击打开 index.html(或用 Chrome 拖入);
  3. 即可看到首页的示例失物招领信息。

基本操作:

  • 浏览 / 搜索:首页直接浏览所有卡片;在顶部搜索框输入物品名称(如「校园卡」)点搜索;用「全部 / 寻物启事 / 招领启事」标签筛选。
  • 发布信息:点右上角「发布信息」,填写信息类型、称呼、标题、物品名称、描述、联系方式,点「立即发布」。
  • 查看详情 / 联系:点卡片上的「查看详情」,可看到完整描述与联系方式,点「📋 一键复制」复制联系方式。
  • 更新状态:在详情页点「✓ 已找到」或「✓ 已归还」,状态实时更新。
  • 我的发布:点「我的发布」,输入发布时填的称呼,即可查看本人发布过的全部信息。

⚠️ 数据保存在浏览器本地存储(localStorage),清理浏览器缓存会导致已发布的数据丢失。

6.3 实现成果展示

① 首页(浏览、搜索、分类筛选)
6-首页

② 发布信息页
1-发布信息页

③ 物品详情页(一键复制联系方式、状态维护)
3-物品详情页

④ 我的发布页
2-我的发布页


七、结对感受与总结

在本次结对编程作业中,我们基于上一轮的原型设计,把校园失物招领的想法实现为可本地直接运行的网页程序。开发使用GitHub仓库协同,一人负责页面布局、HTML骨架和CSS样式美化,另一位负责JS交互逻辑的编写,遇到问题两个人一起讨论调试。

真正动手编码之后,我们感受到原型图和实际代码之间存在不小差距。很多在原型上看着简单的交互,写代码时会遇到各种细节难题,例如本地存储更新后页面如何刷新、弹窗开关控制、剪贴板接口在浏览器中的权限限制等,这些细节在原型设计阶段很难考虑周全。结对编程的好处就是两人可以互相审阅代码,快速发现对方忽略的问题,减少程序缺陷。

同时我们也意识到这个纯前端实现方案的短板:所有数据保存在浏览器本地存储里,如果用户清理浏览器缓存,所有发布信息都会消失。如果后续继续迭代项目,可以增加后端数据库,实现多设备数据互通,增加账号登录模块,进一步完善整个系统。

这次作业完整走完从原型到代码实现、测试、文档编写的流程,锻炼了前端编码、GitHub团队协作、软件测试和项目文档撰写能力。结对编程不是简单拆分任务各自写代码,互相评审、一起讨论方案、协同排查bug,能够更早发现设计上的不足,提升最终软件的质量。


八、Github代码签入记录

下图为本项目GitHub提交记录,每次提交都编写规范commit信息,清晰记录每次改动内容。
5-GitHub签入记录


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

1. 问题一

  • 问题描述:短时间连续多次点击物品卡片,会同时弹出多层详情弹窗,页面交互错乱。
  • 做过哪些尝试:一开始只写弹窗隐藏逻辑,快速点击时事件来不及终止;后面增加全局弹窗状态标记,记录弹窗打开/关闭状态。
  • 是否解决:✅ 已解决
  • 有何收获:弹窗交互必须增加状态锁,限制重复触发;前端开发要考虑用户快速连续点击这类非常规操作。

2. 问题二

  • 问题描述:直接双击本地HTML文件打开网页时,浏览器安全策略会限制剪贴板接口,一键复制联系方式的功能无法正常触发。
  • 做过哪些尝试:查阅官方文档了解本地file协议的权限限制;在复制代码增加try-catch捕获异常,当复制失败时弹出提示给用户。
  • 是否解决:✅ 增加了降级方案,复制失败会提示用户手动复制文本
  • 有何收获:浏览器不同访问方式权限不一样,开发前端功能要提前考虑环境差异,增加异常兜底逻辑,避免功能直接失效。

3. 问题三

  • 问题描述:首页打开后一直停留在「加载中…」,失物招领列表完全不渲染。按 F12 打开控制台,报出一条解析期错误:Uncaught SyntaxError: Identifier 'currentTab' has already been declared (at index.html:43:13)。
  • 做过哪些尝试:先怀疑数据没写进本地存储,检查后发现 localStorage 里数据正常;再把错误定位到具体行——index.html 第 42 行引入的 js/main.js 里已经声明了 let currentTab,而第 43~46 行的内联脚本又重复声明了一次同名变量。JavaScript 顶层作用域不允许用 let 重复声明同一标识符,于是整段内联脚本在解析阶段就抛错,window.onload 里的 renderList() 根本没机会执行,列表于是一直停在「加载中…」。
  • 是否解决:✅ 已解决——删除内联脚本中重复的 let currentTab,只保留 window.onload = function(){ renderList(); };;并用 Node 复现浏览器全局作用域,做了修复前后的对照验证(修复前稳定复现该 SyntaxError,修复后无冲突、首页正常渲染出 3 张卡片)。
  • 有何收获:内联脚本与外部脚本共享同一个顶层作用域,变量名会互相冲突,变量命名要避免「两处都声明」;排查「页面点了没反应 / 白屏」这类问题时,优先看控制台的第一条报错并定位到具体行,比盲目改代码高效得多。这次问题也暴露出多人协作中同一文件被反复覆盖的风险,提醒我们要小步提交、及时同步彼此改动。

修复前的报错现场:

4-单元测试与报错

修复前首页一直停在「加载中…」:
6-首页


十、评价队友

值得学习的地方

队友在页面设计与CSS样式实现方面很有想法,对界面的美观度、细节排版比较敏感,愿意反复调整卡片配色、圆角、间距,让网页看起来整洁舒服。写出来的HTML和CSS代码结构清晰,命名规范。在GitHub协作过程中,每次提交代码都会写清楚commit注释,方便我们回溯每次改动。遇到项目难题时,不会搁置问题,会主动和我沟通想法,一起商量可行的解决方案。在文档撰写阶段,主动收集项目截图,配合梳理博客内容,分工上十分配合,结对沟通效率很高。

需要改进的地方

在JavaScript逻辑开发时,前期思考更多是正常流程下的功能实现,对于各种边界、异常场景考虑偏少,很多问题只有在实际测试的时候才会暴露出来。另外编码习惯上,有时候会一次性写完一整块功能代码再提交,没有拆分成更小的增量分步提交。一旦出现bug,需要在一大段代码里定位错误,增加了调试和代码复审的压力。后续开发可以尝试小步迭代,完成一个微小功能就提交一次,便于结对互相审查,问题也可以尽早发现,避免大量缺陷集中到最后才暴露。

posted on 2026-10-09 20:59  lUoUl  阅读(10)  评论(0)    收藏  举报