软件工程第二次结对作业:校园失物招领的程序实现

这个作业属于哪个课程 H202601软件工程与软件工程实践
这个作业要求在哪里 2026秋软件工程结对作业(第二次)
这个作业的目标 基于前期校园失物招领原型,采用结对编程完成 Web 前端开发,实现发布信息 — 浏览 / 搜索 — 查看详情 — 联系发布者 — 更新物品状态完整业务闭环;完善异常场景交互,记录开发工时与测试过程,完成项目博客撰写。
GitHub项目地址 校园失物招领
结对同学博客链接 雷葭雨博客
参与人员1 102401607 梁倩
参与人员2 112400810雷葭雨

一、具体分工

结对编程分工表(校园失物招领 · 纯前端交互原型)

人员 负责模块 具体工作内容
雷葭雨 页面结构与交互逻辑 1. 搭建页面基础框架:首页列表页、发布信息页、物品详情页
2. 实现发布表单:区分【寻物启事】【招领启事】,录入物品名称、描述、联系方式等信息
3. 详情页交互:展示物品全部信息、联系方式;状态切换按钮(标记已找到/已归还)
4. 页面之间跳转逻辑,保证「发布→浏览→详情→修改状态」完整流程
梁倩 模拟数据、搜索功能、UI美化和原型测试 1. 使用JS数组在前端模拟临时数据
2. 实现关键词搜索功能:根据物品名称过滤展示列表
3. 页面UI美化,统一布局、配色,优化交互体验
4. 原型整体测试,检查流程bug,审查代码,提出优化意见

二、PSP表格

参照作业要求中示例一的小伙伴绘制既有数据也有图示的记录:
PSP表格
造成预估耗时与实际耗时差异的原因主要为

  • 前期预估偏乐观,低估细节工作量:预估阶段只考虑了核心功能(发布、浏览、搜索、详情页状态修改),忽略很多细节交互:表单输入校验、页面样式对齐、列表渲染、状态标记切换的边界情况。这些细碎调试工作没有纳入预估,造成计划、分析、设计、编码与测试阶段时间超出预期。
  • 需求理解与设计复审消耗额外时间:结对讨论时,两人对于页面交互功能逻辑存在一定分歧,在设计复审环节需要反复沟通、调整页面流转逻辑,讨论时间超出预估。同时在生成设计文档时,补充页面草图、数据结构描述,花费了更多时间。
  • 调试排错时间不可预判:JS 模拟数组数据、关键词搜索过滤功能出现 bug,比如搜索关键词大小写、空输入异常、物品状态更新后列表没有同步刷新,定位与修复问题需要额外时间,测试阶段工时增加。
  • 结对编程角色切换带来少量时间开销:两人交换优化后代码时,需要交接代码思路、互相讲解逻辑,该沟通成本前期预估不足;但结对的代码复审环节也减少了后期重大 bug,控制了整体超时幅度。

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

1.1 现实困扰

WEB解决的核心问题是:把分散、易丢失、难触达的寻物 / 招领信息集中到一个平台,并通过 "智能匹配" 主动让失主与拾获者 "相遇",而不是被动等待。

1.2 代码实现思路

我们先把系统拆成一条完整的用户闭环:

浏览现有信息 → 搜索或拍照查找 → 没找到则发布
→ 系统自动匹配 → 查看匹配依据 → 联系对方
→ 物品归还/找到 → 标记完成 → 感谢与积分

在此基础上再拆分为数据层、页面导航层和功能层:

  • 数据层保存当前会话的物品、消息、积分和搜索历史。
  • 导航层负责页面进入、返回、底部标签和通用卡片。
  • 功能层分别处理发布、识图、匹配、搜索、详情、聊天和感谢信。
  • 启动层只负责页面初始化、时间、网络和电池等状态。

这种拆分避免将全部代码继续堆在一个 HTML 文件中,也方便其他测试人员按功能定位问题。

2. 关键实现的流程图

图 1:系统总体架构与数据流

flowchart LR U["用户操作<br/>点击 / 输入 / 拍照"] --> R["路由层 pageStack<br/>navigate / backPage / switchTab"] R --> L["业务逻辑层<br/>发布 / 搜索 / 匹配 / 积分"] L --> D[("数据层 DATA<br/>失物 + 招领信息")] D --> L L --> V["渲染函数<br/>cardHTML / renderXxx"] V --> UI["DOM 视图"]

数据整体按“用户操作—业务处理—数据更新—页面渲染”的方向流动:用户操作经路由层进入业务逻辑层,逻辑层读写 DATA,再通过渲染函数把结果反映到 DOM,使视图始终由当前数据状态生成。

图 2:页面路由与跳转关系

flowchart TD Home["首页 page-home"] -->|点击卡片| Detail["详情页 page-detail"] Home -->|底部凸起发布按钮| Publish["发布页 page-publish"] Home -->|搜索框| Search["搜索页 page-search"] Home -->|校园地图入口| Map["地图页 page-map"] Search -->|点击结果| Detail Publish -->|提交| Success["发布成功弹窗"] Success -->|查看匹配 / 详情| Detail Detail -->|站内联系| Chat["聊天页 page-chat"] Message["消息中心 page-message"] -->|点击通知| Detail Mine["我的 page-mine"] -->|点击卡片| Detail Chat -.返回.-> Detail Detail -.返回.-> Home

pageStack 保存用户访问过的页面,navigate() 进入新页面,backPage() 返回上一层,switchTab() 负责底部主导航切换。消息、搜索结果和“我的”中的物品都复用同一个详情页。

图 3:发布信息数据流

03-publish-match-flow

表单只有在类型、名称、类别、地点、时间和联系方式全部通过校验后才会生成数据。新信息写入 DATA 后立即与相反类型的信息匹配,匹配成功时双方记录都会保存关联,并生成消息提醒。

图 4:拍照识物数据流

04-photo-recognition-flow

OCR 用于发现卡号、书名、品牌等文字,Canvas 提供颜色和形状线索;拍照搜索还会使用 MobileNet 中间特征比较图片。模型不可用时退回本地图像指纹,避免主流程中断。

图 5:智能匹配打分流程

GIF_20261009155429850

3. 重要代码片段与解释

片段 1:页面栈路由——实现原生 App 式前进与返回动画

let pageStack = ['page-home'];

function showPage(id, anim){
  const page = $(id);
  document.querySelectorAll('.page').forEach(p=>
    p.classList.remove('active','slide-in','slide-back','fade-in')
  );
  page.classList.add('active');
  if(anim==='slide') page.classList.add('slide-in');
  else if(anim==='fade') page.classList.add('fade-in');
}

function navigate(id, anim){
  pageStack.push(id);
  showPage(id, anim||'slide');
}

function backPage(){
  if(pageStack.length<=1){ switchTab('home'); return; }
  const cur = pageStack.pop();
  const target = pageStack[pageStack.length-1];
  $(cur).classList.add('slide-back');
  $(target).classList.add('active');
  setTimeout(()=>$(cur).classList.remove('active','slide-back'),280);
}

function switchTab(name){
  pageStack = ['page-'+name];
  showPage('page-'+name,'fade');
}

pageStack 保存当前页面的访问路径。进入详情、搜索或发布页时,navigate() 把页面编号压入栈并播放向左滑入动画;点击返回时,backPage() 弹出栈顶页面,恢复前一页并播放向右退出动画。切换底部主标签时则重新建立栈,防止从“我的”返回到已经无效的搜索层级。整个路由不依赖第三方框架,却保留了接近原生 App 的前进、返回和淡入体验。

片段 2:列表卡片统一渲染——首页、搜索和“我的”复用

function cardHTML(item){
  const isLost = item.type==='lost';
  const done = item.status==='done';
  const thumb = item.img
    ? `<div class="info-thumb ${done?'thumb-done':''}">
         < img src="${item.img}" alt="">
       </div>`
    : `<div class="info-thumb ${isLost?'thumb-lost':'thumb-found'}
         ${done?'thumb-done':''}">${item.emoji}</div>`;

  return `<div class="info-card" onclick="goDetail(${item.id})">
    ${thumb}
    <div class="info-main">
      <div class="info-tags">
        <span class="tag-type ${isLost?'tag-lost':'tag-found'}">
          ${isLost?'寻物':'招领'}
        </span>
        ${done?'<span class="tag-status done">已完成</span>'
              :'<span class="tag-status">进行中</span>'}
        ${isLost&&item.reward>0
          ?'<span class="tag-reward">悬赏 ¥'+item.reward+'</span>':''}
      </div>
      <div class="info-name">${item.name}</div>
      <div class="info-sub">${item.place} · ${item.pubTime}</div>
    </div>
  </div>`;
}

// 不同页面只负责筛选数据,卡片结构由同一个函数生成
// renderHome:list.map(cardHTML)
// doSearch:results.map(cardHTML)
// renderMine:list.map(cardHTML)

首页、普通搜索、拍照搜索和“我的”页面展示的都是同一种物品信息,因此统一使用 cardHTML()。函数根据图片、寻物/招领类型、完成状态和悬赏金额自动组合缩略图与标签。各页面只负责筛选数据,不再复制卡片 HTML;以后修改标签样式或增加字段时,只需要修改一个函数即可保证所有列表一致。

片段 3:关键词搜索——多字段匹配与结果高亮

function doSearch(){
  const kw = $('search-input').value.trim();
  if(!kw){ toast('请输入要搜索的内容'); return; }

  searchHistory = [kw]
    .concat(searchHistory.filter(h=>h!==kw))
    .slice(0,8);

  const results = DATA.filter(d=>
    d.name.includes(kw) ||
    d.category.includes(kw) ||
    d.place.includes(kw) ||
    d.desc.includes(kw)
  );

  $('result-list').innerHTML = results.length
    ? results.map(item=>{
        let card = cardHTML(item);
        return card.replace(
          item.name,
          `<span class="kw-hl">${item.name}</span>`
        );
      }).join('')
    : emptyHTML('没有找到相关信息,换个关键词试试');
}

搜索不是只检查物品名称,而是同时匹配名称、分类、地点和详细描述。例如搜索“京元餐厅”可以找到名称中没有“餐厅”但地点符合的信息。搜索词会去重后加入最近历史,并只保留最近 8 条;有结果时继续复用 cardHTML(),再给物品名称包裹 kw-hl 高亮样式,让用户能够快速定位结果。无结果时显示统一空状态,而不是留下空白页面。

四、附加特点设计与展示

这一部分选择四个直接服务于“更快找回失物”和“鼓励校园互助”的特色功能进行展示。

4.1 拍照识物——上传图片自动识别物品类别

创意与意义

发布失物信息时,用户经常不知道应该选择“数码电子”“校园卡证件”还是“其他”。拍照识物允许用户直接上传照片,系统通过 OCR 读取卡面、书名、品牌等文字,再结合图片颜色和外观特征推断类别并自动填入表单。它减少了手动输入步骤,也使后续搜索和匹配获得更统一的数据。

实现思路

图片先经 FileReader 转为页面可处理的数据地址;runRecognize() 并行考虑 OCR 文字和图像基础特征;inferCategory() 使用关键词规则推断物品类别、建议名称和置信度。OCR 超时或没有识别到文字时,用户仍可手动选择类别,不会阻断发布。

async function runRecognize(file,dataUrl){
  let ocrText='';
  try{
    const rec=await Promise.race([
      Tesseract.recognize(file,'eng'),
      new Promise((_,reject)=>
        setTimeout(()=>reject(new Error('ocr-timeout')),15000))
    ]);
    ocrText=rec.data.text.trim();
  }catch(err){
    ocrText='';
  }
  let feat={};
  try{ feat=await analyzeImage(dataUrl); }catch(err){ feat={}; }
  return inferCategory(ocrText,feat);
}

这段代码的价值在于加入了超时和异常分支。第三方 OCR 即使加载失败,程序仍会继续分析图片并允许用户完成发布。

效果展示

GIF_20261009155429850

4.2 智能匹配——丢失和拾取信息自动碰撞

创意与意义

用户发布信息后不必每天重复搜索。系统会让“寻物”和“招领”信息自动碰撞,综合图片、类别、地点、时间和关键词计算匹配度;达到 60 分就生成候选并写入消息中心。这样新发布的信息可以主动唤醒已有线索,把“人找信息”变成“信息主动找人”。

实现思路

findMatches() 先排除自身和已完成信息,再调用 calcMatch();同为寻物或同为招领的数据会提前返回。候选达到门槛后,双方记录都会保存关联信息,同时生成包含最高匹配依据的提醒。

const matches=await findMatches(item);
if(matches.length){
  item.matches=matches;
  matches.forEach(m=>{
    MESSAGES.unshift({
      t:'智能匹配:你的「'+name+'」可能找到了',
      d:'匹配度 '+m.score+'%,'+m.reasons[0]+',点击查看',
      time:'刚刚',
      itemId:item.id
    });
  });
}

提醒不仅给出百分比,还显示“图片相似度”“物品类别相同”或“地点相同”等原因,让用户知道系统为什么推荐该条信息。

效果展示

4.3 悬赏任务——提高急需物品的找回机会

创意与意义

校园卡、钥匙、耳机等物品可能急需找回。发布寻物时可以选择悬赏金额,首页卡片和详情页都会突出显示悬赏状态,使紧急信息更容易被注意,也能鼓励更多同学主动提供线索。悬赏只对寻物启事开放,招领信息不出现该入口,避免功能语义混乱。

实现思路

选择“寻物启事”后才显示悬赏选项,金额保存在 pubForm.reward。提交时再次按类型判断;首页可以单独筛选悬赏信息,详情页则根据完成状态显示“等待找回”或“悬赏已兑现”。

function selectPubType(type){
  pubForm.type=type;
  $('f-reward-row').style.display=type==='lost'?'flex':'none';
}

const item={
  // 其他发布字段
  reward:pubForm.type==='lost'?(pubForm.reward||0):0
};

if(homeSeg==='reward') return d.reward>0;

这里同时在界面层和数据层限制悬赏类型。即使表单状态异常,招领信息写入数据时悬赏金额仍会归零。

效果展示

GIF_20261009155429850

4.4 归还致谢——感谢信与爱心积分形成正向反馈

创意与意义

失物招领不应该在“物品已归还”处突然结束。完成后,失主可以发送自动生成且允许修改的感谢信;拾获者收到感谢后获得爱心积分。它将一次物品归还转化为可见的正向反馈,让帮助他人的行为得到回应,鼓励校园中的善意继续传递。

实现思路

markDone() 更新物品状态,并根据寻物或招领类型显示不同提示;thanksTpl() 根据物品名称生成感谢信初稿;submitThanks() 保存感谢记录并更新积分和页面。

function markDone(id){
  const d=DATA.find(x=>x.id===id);
  d.status='done';
  if(d.type==='found') addPoints(20);
  renderDetail(id);
  renderHome();
  renderMine();
}

function submitThanks(){
  const d=DATA.find(x=>x.id===currentThanksId);
  const text=$('th-text').value.trim();
  if(thanksMode==='send'){
    if(!text) return;
    
thankLetters.unshift({item:d.name,text,dir:'sent'});
  }else{
    thankLetters.unshift({item:d.name,text,dir:'received'});
    addPoints(10);
  }
}

状态更新后会同步刷新详情、首页和“我的”页面,避免不同页面显示不一致。感谢信和积分只保存在当前演示会话中,刷新后会恢复初始数据。

效果展示

图1说明 图2说明

五、目录结构与使用说明

1. 目录组织

campus-lost-found-web/
├── 校园失物招领小程序.html       # 实际演示页
├── assets/                         # 地图、头像等静态资源
├── css/
│   ├── style.css                      # CSS 统一入口
│   ├── base.css                       # 变量、手机外壳、通用导航
│   ├── home.css                       # 首页、卡片、消息和地图
│   ├── forms.css                      # 发布表单和搜索页
│   ├── detail.css                     # 详情、“我的”、弹窗和 Toast
│   ├── prototype.css                  # 桌面端需求说明面板
│   └── features.css                   # 聊天、识图、匹配、悬赏、感谢信
├── js/
│   ├── app.js                        # 初始化、时钟、天气、电池和网络状态
│   ├── core/
│   │   ├── data.js                   # 演示数据、分类图标和 ID
│   │   └── navigation.js             # 路由、Toast、通用卡片和首页
│   └── features/
│       ├── publish.js                # 发布表单、图片上传和选择弹层
│       ├── vision.js                 # OCR、MobileNet、图像指纹和向量相似度
│       ├── matching.js               # 五维加权匹配与提交发布
│       ├── search.js                 # 普通文本搜索
│       ├── photo-search.js           # 拍照搜物和图片相似度排序
│       ├── detail.js                 # 物品详情和匹配结果
│       ├── gratitude.js              # 积分、完成状态和感谢信
│       ├── chat.js                   # 站内聊天
│       ├── profile.js                # 我的页面
│       └── messages.js               # 消息中心
├── docs/                           # 架构和依赖说明
└── tests/                          # 手工回归用例与测试素材说明

2. 如何运行

方式一:克隆后直接体验

先将远程仓库下载到电脑,再切换到本项目所在的子目录:

git clone https://github.com/KongCheng06/102401607-112400810.git
cd 102401607-112400810/campus-lost-found-web

找到该目录中的 校园失物招领小程序.html,选择 Chrome 或 Edge 打开。

这种方式不涉及 Node.js、数据库或包管理器。由于 OCR 和图片特征模型通过网络加载,体验识图功能时请保持联网。

方式二:通过静态服务访问

若直接打开 HTML 时出现资源无法加载或浏览器安全限制,可在 campus-lost-found-web 目录中执行:

python -m http.server 8000

服务启动后,在浏览器输入 http://localhost:8000。该方式需要电脑已安装 Python 3,适合进行识图或兼容性测试。

六、单元测试

6.1 工具选择与学习方法

我们使用 Node.js 内置的 node:test 和 node:assert/strict,不依赖第三方测试框架。学习过程是先阅读 Node.js Test Runner 官方文档,再学习邹欣老师的《单元测试与回归测试》,最后阅读项目源码,用语句覆盖、判定/分支覆盖、条件覆盖、主要路径覆盖和边界值分析设计测试。

本地运行:

npm test

查看覆盖率:

npm run test:coverage

6.2 白盒测试用例

目前共有 14 个自动化用例:

编号 被测路径 预期结果
WB01 同义词循环命中与退出 同义词归一且不重复
WB02 空值和未命中路径 返回空关键词数组
WB03 区域循环命中/不命中 正确判断同一区域
WB04 地点相同、包含、同区域、无关 返回对应状态
WB05 地点为空的防御路径 不误判为地点相同
WB06 所有时间桶及默认路径 返回对应时间键或空串
WB07 同类型提前返回 返回 null
WB08 图片及其他条件全部命中 严格按 50/20/15/10/5 计分
WB09 同区域分支 地点获得 10 分
WB10 缺少图片分支 剩余可用权重正确归一化
WB11 所有条件不命中 得 0 分且无匹配理由
WB12 60 分边界 60 分保留,50 分排除
WB13 自身、完成项、同类项过滤 只保留有效候选并降序排列
WB14 无候选路径 返回空数组且不抛异常

6.3 部分单元测试代码

test('WB12:60分边界保留、低于边界排除', async () => {
  const query = item();
  const exactBoundary = item({
    id: 2,
    type: 'found',
    name: '黑色物品',
    desc: '无明显特征',
    place: '第一田径场'
  });
  const belowBoundary = item({
    id: 3,
    type: 'found',
    place: '第一田径场',
    time: '昨天'
  });
  const data = [query, exactBoundary, belowBoundary];
  const {calcMatch, findMatches} = loadMatching({visual:null, data});

  assert.equal((await calcMatch(query, exactBoundary)).score, 60);
  assert.equal((await calcMatch(query, belowBoundary)).score, 50);
  assert.deepEqual((await findMatches(query)).map(x=>x.id), [2]);
});

calcMatch() 负责计算两个条目的匹配度,findMatches() 负责过滤候选并执行 60 分门槛。该用例直接从规则反推输入,使结果刚好位于边界两侧。

6.4 测试数据与“刁难测试”

我们使用对象工厂生成一条字段完整的默认数据,每个用例只修改当前分支需要的字段。数据同时包含:

  • 正向数据:类别、地点、时间和关键词相同;
  • 反向数据:跨区域、不同时间桶、无共同关键词;
  • 异常数据:null、空字符串、无图片、无候选;
  • 状态数据:自身、同类型、已完成和多个有效候选;
  • 边界数据:精确构造 60 分、低于 60 分和最高 99 分。

针对测试人员可能使用的重复同义词、大小写混合英文、空地点、模型失败、同一记录匹配自己、已完成记录重新推荐、多条候选乱序等情况,测试中均有相应分支。白盒分析还实际发现并修复了“两地点均为空时被误判为相同”的问题。

6.5 自动化与测试评价

.github/workflows/unit-tests.yml 会在 push、Pull Request、手动触发及每天北京时间 08:00 运行测试。当前实测结果:

tests 14
pass  14
fail  0

matching.js 行覆盖率:62.96%
matching.js 分支覆盖率:96.67%
matching.js 函数覆盖率:81.25%

这 14 个用例可以满足匹配核心的单元测试要求,但不能单独证明整个网页完全正确。未覆盖部分主要是依赖 DOM 的发布和弹窗逻辑;MobileNet 加载、图片解码、文件选择器、视觉布局和浏览器兼容性还需要结合 tests/MANUAL_TEST_CASES.md 进行集成与手工回归。

更完整的测试设计见 docs/UNIT_TEST_GUIDE.md。

七、GitHub代码签入记录截图

GitHub提交记录截图
GitHub提交记录截图

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


1. “我的”页面新增感谢信模块起初无法滚动及折叠,导致屏幕下方导航栏被遮挡无法继续测试,后续为感谢信模块新增滚动及折叠效果后成功解决。

2.会话逻辑异常。起初在他人发布的寻物启事点击会话联系后,演示效果为贴主发出第一句话,我回答(如下左图);后经修改改为由“我”主动联系,贴主进行回复(如下右图)

图1说明 图2说明
3.消息中心跳转逻辑异常。由消息中心的“有人发起联系”卡片点进去由原本的进入帖子详情页面改为直接跳转到会话页面。

4.结对开发时,两人同时修改页面 JS 与样式文件,出现代码合并冲突。后使用 Git 查看冲突标记,手动合并冲突代码。

收获

1.前端页面布局要考虑内容溢出边界问题,长内容区域需要做好滚动、折叠等边界处理,避免遮挡页面基础组件。
2.业务逻辑要贴合真实使用场景,不能只完成基础页面渲染,要仔细核对交互流程、数据流向。
3.结对开发需要提前划分模块,做好沟通;Git 冲突需要读懂标记,谨慎合并代码,避免覆盖队友代码。
4.功能迭代时要做好前后效果对比,方便调试、留存项目文档用于报告撰写。

九、对队友的评价

对112400810【雷葭雨】的评价

1.值得学习的地方

  • 业务理解清晰,能够独立完成首页、发布页、详情页的页面框架搭建,实现表单录入、页面跳转等核心交互,按时交付负责模块。
  • 沟通积极性强,遇到交互逻辑问题会主动交流,代码结构整洁,方便后续联调,能够配合完成整体原型测试。

2.需要改进的地方

  • 开发前期对异常场景考虑不够充分,部分边缘交互逻辑缺少判断,测试阶段才发现问题,增加了调试时间。
  • 编写代码时注释偏少,部分复杂跳转逻辑没有说明,他人阅读和维护代码时理解成本较高。

十、总结

1.程序开发层面
本次校园失物招领系统开发,完整经历需求分析、设计、前端编码与联调测试。开发中体会到业务状态流转是难点,帖子和认领申请存在多种状态,容易出现逻辑漏洞。同时意识到不能只依赖前端校验,还要考虑图片上传、页面适配等用户体验细节,充分认识到前期设计对项目稳定性的重要作用。
2.结对合作层面
结对开发让我体会团队协作的意义。我们按能力分工,分别负责与前端开发和测试、文档整理,但开发时容易出现需求理解不一致、逻辑不合理等问题。及时沟通、定期同步是关键。自己写的代码很难发现问题,同伴可以快速找到隐藏 bug;同时需要统一代码规范,协调开发节奏,保障项目有序推进。
3.综合能力与工程思维感悟
项目不仅锻炼编码能力,更培养软件工程思维。开发面向校园用户的系统,除实现基础功能外,还需要考虑信息安全与用户隐私保护。规范的注释与项目文档,方便后期调试和撰写报告。软件开发是迭代完善的过程,真实项目大多需要团队协作,沟通能力和工程规范和代码能力同样重要。

posted @ 2026-10-09 11:35  空城空旧忆丶  阅读(8)  评论(0)    收藏  举报