2026秋软件工程结对作业(第二次之程序实现)|拾见校园

项目 内容
这个作业属于哪个课程 H202601 软件工程与软件工程实践
这个作业要求在哪里 结对作业第二次:程序实现
本作业博客 本篇作业博文
这个作业的目标 把已有的失物招领原型实现为可运行程序,走通发布、查找、联系和处理完成的流程,并用测试检查规则和异常情况
本人 姚华俊,082403146
结对同学 黄锦培,102401221;同学本次作业博文;GitHub
项目仓库 Grim761/082403146-102401221
同学的 fork / PR coderbrain404/082403146-102401221 · 测试提交 4e9d0c2 · PR #1
在线体验 拾见校园程序实现版(本机保存模式)
上一次原型 拾见校园交互原型

一、从原型到程序,这次补上什么

上一次作业把“拾见校园”改成了校园线索墙:每件物品是一张卡片,先展示类别、地点、时间和状态,再进入详情核对。我希望这次实现能够保留这个方向。失物招领页面最有用的内容,往往是“东西在哪里”“什么时候发生”“还需要联系吗”,这些信息应该比装饰更容易被看到。

原型里的按钮可以跳转,但点击成功并不代表数据保存成功。比如,刚发布的信息刷新后消失,或者同学已经领回物品,列表仍显示“招领中”,页面再完整也不能把事情处理完。这次程序实现重点补上了三件事:数据保存、发布者归属、各个页面对同一状态的展示。

实现采用 HTML、CSS、JavaScript,另外提供一个不依赖第三方库的 Node 共享服务。选择这套方式是为了让下载后的页面容易运行,也方便把业务函数单独拿出来测试。范围仍然围绕校园失物招领,没有加入聊天、地图、实名认证和后台管理。

拾见校园线索墙

二、已经实现的使用流程

1. 发布寻物或招领

发布者先选择“我丢东西了”或“我捡到东西了”,再填写物品名称、类别、遗失或拾取时间、区域、具体地点和联系或领取信息。补充描述和图片可选。名称、地点和联系信息不能只填空格,时间不能明显在未来。

表单右侧会同步出现一张线索卡。这个预览主要用来检查标题是否含糊、地点是否写得足够具体。未提交的文字会保存为草稿,刷新或重新进入发布页可以恢复;图片需要重新选择,避免大图草稿占满浏览器存储。保存成功后进入对应详情,而不是只弹出一句“发布成功”。

发布页面与卡片预览

2. 浏览、搜索和查看详情

首页默认显示仍在处理的线索,可以切换寻物、招领,也可以按物品类别和校园区域共同筛选。搜索会检查名称、描述和地点等字段。输入“图书馆 钥匙”时,两段关键词需要同时匹配,能比只搜“钥匙”更快缩小范围。

详情页把发生时间、区域、具体地点和当前状态放在一起。联系信息可以直接复制;招领信息也可以只写公共领取地点,例如“已交至图书馆一楼服务台”。页面提醒核对独有特征,描述中不宜公开完整证件号码。

线索详情与领取信息

3. 由发布者更新处理结果

寻物和招领使用同一套底层状态:open 表示仍在处理,resolved 表示已经完成。显示时再结合类型生成不同文字:寻物完成显示“已找到”,招领完成显示“已归还”。

发布者可以在详情页或“我的发布”更新状态。完成后,默认列表收起这条线索,详情页收起联系信息;勾选“包含已完成”可以查看历史。更新前有确认提示,误操作时也能重新开放。共享服务会检查当前账号是否为发布者,不能只靠页面隐藏按钮来限制权限。

标记已归还后的详情

收起联系方式只是界面行为,原字段仍保存在数据中,并不等于已经删除公开过的信息。

三、结对安排与 PSP

1. 分工安排

成员 开发计划中的分工 可从仓库核对的执行记录
姚华俊 页面和交互流程、发布表单、即时预览、运行说明与博文整理 Grim761 账号在主仓库按业务规则、共享服务、页面流程、文档和测试分阶段提交;线下工作时长不能仅由提交记录推断。
黄锦培 搜索筛选和状态规则复核、边界测试、共享流程检查、README 复核 coderbrain404 账号在个人 fork 补两项服务端文件存储回归测试,更新测试入口、README 和报告,创建 PR #1;本地 43 项测试通过。

当前主仓库已有分阶段的功能提交;黄锦培的 fork 分支 codex/store-recovery-tests 已推送,并创建了指向 Grim761:main 的 PR #1。PR 已于 2026-10-10 22:33(北京时间)合并到主仓库 main,合并提交为 669c4b8。

2. PSP 表

编码前在项目的开发计划中记录了本轮工作预估,单位为分钟。实际列是根据完成的工作回顾估算的个人投入时间,不是计时器记录;AI 辅助生成代码和文档的运行时间不单独计入。Development、Reporting 是小计,不重复计入合计。

PSP2.1 阶段 预估耗时 本人耗时(回顾估算)
Planning 计划 15 10
· Estimate 估计本次任务需要多少时间 15 10
Development 开发 405 325
· Analysis 需求分析与技术学习 40 35
· Design Spec 生成设计文档 25 20
· Design Review 设计复审 15 15
· Coding Standard 代码规范 10 5
· Design 具体设计 35 30
· Coding 具体编码 180 110
· Code Review 代码复审 25 20
· Test 测试与修复 75 90
Reporting 报告 60 60
· Test Report 测试报告 25 30
· Size Measurement 工作量统计 10 5
· Postmortem & Process Improvement Plan 事后总结与改进计划 25 25
合计 480 395 分钟(约 6 小时 35 分)

与预估相比,AI 辅助让页面与服务代码初稿更快形成;测试与修复反而比预估多用了时间,因为需要核对草稿跳转、存储失败、发布者权限和跨账号流程。上述数字是回顾估算,缺少逐阶段实时计时记录。下一次在任务开始和切换时记时,才能更准确地分析偏差。

四、程序怎样组织

1. 把页面和业务规则分开

页面入口是 index.html,app.js 处理页面渲染、表单和弹窗;core.js 放字段校验、搜索和状态更新等规则;storage.js 负责本机保存;api.js 负责识别运行模式和请求共享接口;illustrations.js 负责物品示意图和类别匹配。共享服务再分成 HTTP 接口与文件存储两个模块。

这样划分之后,搜索是否匹配、其他人能否修改状态,不需要通过点击页面才能检查。相同的规则既能被浏览器调用,也能被 Node 测试和服务端调用。

数据流程与模块划分

2. 每条线索的数据

一条记录除了物品信息,还需要保存发布者、状态以及两个不同用途的时间。eventAt 是遗失或拾取时间,createdAt 是发布时间,updatedAt 记录最近一次状态变化。这三个时间不能混为一谈:前者帮助核对事件,后两者帮助判断信息的新旧和处理进度。

{
  id, ownerId, ownerName,
  kind, status,
  title, category, zone, place,
  eventAt, description, contact, photo,
  createdAt, updatedAt, demo
}

demo 用来区分初始示例和新发布的信息。共享模式中的 ownerId 由服务端根据登录会话确定,客户端填写一个别人的 ID 并不能改变线索归属。

3. 发布、查看和状态更新的连接

完整使用流程

发布时先校验输入,再保存数据,保存确认成功后才进入详情。状态更新也一样:检查归属和目标状态,保存成功后重新读取列表,再让线索墙、详情和“我的发布”显示同一个结果。联系和交接在线下完成,程序负责把线索和处理状态记录清楚。

五、几段值得说明的代码

1. 同一函数检查状态与发布者

changeStatus 中,缺失记录、归属不符和非法状态分别走不同失败分支。已完成的记录再次设为已完成,不会重复制造更新时间;真正变化时返回新对象,不修改原记录。

function changeStatus(post, actorId, nextStatus, now = Date.now()) {
  if (!post) throw new DomainError('这条线索不存在或已被移除。', 'NOT_FOUND', 404);
  if (!actorId || post.ownerId !== actorId)
    throw new DomainError('只有发布者可以修改这条线索。', 'FORBIDDEN', 403);
  if (!['open', 'resolved'].includes(nextStatus))
    throw new DomainError('状态参数无效。');
  if (post.status === nextStatus) return { ...post };
  return { ...post, status: nextStatus, updatedAt: new Date(now).toISOString() };
}

页面根据当前身份决定是否显示按钮,服务端仍然再次调用这段规则。前者让页面容易使用,后者负责拒绝越权请求。

2. 保存成功之后才替换内存中的数据

本机存储可能因为配额不足而失败。如果先修改内存,再写浏览器存储,页面就可能显示一条没有真正保存的记录。因此 commit 先执行 setItem,成功之后才替换 state。

function commit(next) {
  if (persistent) {
    try { storage.setItem(KEY, JSON.stringify(next)); }
    catch (_) {
      throw new Core.DomainError(
        '本机存储空间不足,信息尚未保存。请缩小图片或导出旧记录。',
        'STORAGE_FULL', 507
      );
    }
  }
  state = next;
}

测试把 setItem 改为抛出配额错误,确认发布失败后旧列表仍然不变。共享存储使用相似的顺序:写临时文件、重命名替换、再更新内存快照。它只用于一个服务进程,不能当作多实例数据库。

3. 发布成功后避免旧草稿再次写回

草稿会在离开发布页时保存,但成功发布属于另一个场景:这时旧草稿应该清掉。实现中为成功发布设置一次性的 skipDraftSave,hashchange 时跳过旧表单保存,随后恢复普通行为。

window.addEventListener('hashchange', () => {
  clearTimeout(draftTimer);
  if (!state.skipDraftSave) saveDraft();
  state.skipDraftSave = false;
  void route();
});

这个细节涉及事件顺序。只在提交按钮里清空草稿还不够,后续跳转事件也可能把旧内容重新保存。

六、两个实用的额外功能

1. 按类别、区域和关键词共同筛选

校园中相似物品很多。“校园卡”会匹配很多线索,但“图书馆+证件卡片+招领”更接近具体需求。因此筛选条件共同生效,并且默认只看未完成记录,避免旧信息干扰。

关键词先去掉首尾空格、转为小写,再按空格拆分。每个词都需要在名称、描述或地点等字段中出现:

const words = text(filters.query).toLocaleLowerCase().split(/\s+/).filter(Boolean);
const haystack = [post.title, post.description, post.zone, post.place, post.category]
  .join(' ').toLocaleLowerCase();
return words.every(word => haystack.includes(word));

类别和区域各自还有严格匹配的判断。自动测试用两个都带 USB 的记录验证大小写和多关键词;组合筛选用九条示例检查“招领+图书馆+证件卡片+处理中”只返回预期的一条。

2. 复制联系或领取信息

同学查看详情后,经常需要把领取地点发给自己或保存下来。用复制按钮可以减少选中文字的操作,手机上也更方便。

实现优先调用浏览器剪贴板 API:

if (!navigator.clipboard?.writeText) throw new Error('FALLBACK');
await navigator.clipboard.writeText(post.contact);

API 不可用或调用失败时,会尝试通过临时文本框复制;两种方式都失败才提示手动选取,不会直接显示复制成功。实际浏览器检查中,复制后的文本与详情里的领取信息一致。完成线索不再显示复制按钮,减少后续重复联系。

七、界面保留自己的特点

这次仍然使用纸色背景、深蓝标题和陶土色提示,线索卡上的编号、文字类别标记、地点和时间形成固定位置。寻物用蓝色,招领用陶土色,完成状态再用偏绿的提示。状态除了颜色,还直接写出文字,避免只靠颜色判断。

字体使用系统自带字体。没有上传照片时,卡片会根据物品名称和类别显示钥匙、雨伞、耳机、校园卡等 SVG 物品示意图;上传照片后优先显示照片。详情页会注明“物品示意图”,避免把示意图当成实拍照片。页面没有依赖外部字体库。桌面列表是三列,窄屏变成一列;发布页在桌面把表单和预览并排,手机上上下排列。内置浏览器中检查了 1440px 和 390px 两个断点,均未出现横向溢出。

我更希望它像一面可查找、可更新的校园信息墙。卡片不需要放很多按钮,先把信息看清楚,再到详情完成下一步操作。

八、测试工具、用例与结果

1. 怎样运行自动化测试

使用 Node 内置的 node:test 和 node:assert/strict,不需要额外安装测试框架。在项目根目录运行:

npm test
node --test --experimental-test-coverage tests/core.test.js tests/storage.test.js tests/store.test.js tests/server.test.js

test 定义一项测试,assert.equal 检查单个结果,assert.deepEqual 比较列表,assert.throws 和 assert.rejects 检查同步、异步失败。接口测试启动临时端口,使用临时目录,结束后关闭服务,不会改动实际运行的数据。

例如,修改权限的测试直接检查 changeStatus 的失败分支:

test('其他用户与未登录用户:不能修改他人的发布', () => {
  for (const actor of ['student-b', '', null]) {
    assert.throws(
      () => C.changeStatus(post(), actor, 'resolved'),
      e => e.code === 'FORBIDDEN'
    );
  }
});

2. 数据怎样设计

测试并非只准备一组能正常发布的数据。先阅读函数中的判断,再选择成功、失败和边界输入,特别检查会影响保存或权限的地方。

重点用例 数据设计 预期
有效发布 完整的寻物 / 招领字段 保存记录、生成正确初始状态
必填与空格 逐项置空;名称、地点和联系信息只填空格 返回对应字段错误
标题边界 40 个字、41 个字 前者通过,后者拒绝
事件时间 无法解析的日期、明显在未来的时间 拒绝提交
多词与组合筛选 usb 黑色;类型+类别+区域+状态 所有条件共同满足
发布者权限 本人、其他人、未登录、记录缺失 本人成功,其余明确失败
保存失败 模拟浏览器配额错误 不新增一条未保存记录
刷新后的数据 重新创建存储对象 身份、发布和完成状态保留

附录列出 18 项业务白盒测试。另有 9 项本机存储测试、2 项服务端文件存储测试和 14 项接口与集成测试,完整输入设计可查看主仓库中的测试报告。

3. 已运行的结果与范围

合并后的版本在本地 Node v24.16.0 上运行结果为 43 项通过、0 项失败,其中 29 项是单元测试,14 项是接口与集成测试。业务、示例、本机存储与服务端模块合计行覆盖率为 94.72%、分支覆盖率 76.89%;这一统计不包含页面 DOM 脚本,不能理解成整个项目都已覆盖。完整命令、输入设计和结果见测试报告。

浏览器走查实际检查了空表单提示、文字草稿恢复、发布后清空草稿、联系信息复制、图片发布与刷新、寻物已找到、招领已归还,以及另一个账号没有更新他人记录的入口。

上述交互操作使用 Edge 和内置浏览器。本次另从个人 fork 下载完整 ZIP,解压后用 Chrome 的无界面模式直接打开 index.html,确认“本机保存”和线索墙;无结果搜索、发布进入详情、刷新后保留、本人标记已找到和已完成筛选均通过。查看本机模式截图。共享服务在两个独立 Chrome 配置中检查了 A 发布、B 查看、B 更新被拒绝(403),以及 A 更新后 B 刷新看到完成状态。Chrome 中的图片选择、剪贴板实际写入和日常操作体验仍需人工复核。

Chrome 中账号 B 查看账号 A 的线索

账号 A 更新后,Chrome 中账号 B 刷新看到已找到

PR 合并提交 669c4b8 的 GitHub Actions 测试与静态页面发布均已成功;本地 43 项测试结果和覆盖范围见上文。

九、目录与运行说明

082403146-102401221/
├── index.html
├── css/style.css
├── js/
│   ├── core.js
│   ├── seed.js
│   ├── storage.js
│   ├── api.js
│   ├── illustrations.js
│   └── app.js
├── server/
│   ├── server.js
│   └── store.js
├── tests/
│   ├── core.test.js
│   ├── storage.test.js
│   ├── store.test.js
│   └── server.test.js
├── docs/
├── tools/draw_diagrams.py
├── .github/workflows/
├── README.md
└── package.json

1. 本机体验

从 GitHub 下载 ZIP,完整解压,用 Chrome 打开 index.html;css、js 文件夹要一起保留。页面显示“本机保存”时,记录属于当前浏览器,不会同步到其他设备。页脚可以导出本机备份。在线静态页面也是这个模式。

本地文件的存储行为由浏览器决定。如果禁用存储,会显示“临时模式”并说明关闭页面后丢失;需要固定来源时,可以使用在线页面或下面的本地服务。

2. 共享体验

安装 Node.js 22 或更新版本,在项目目录运行:

npm start

无需 npm install,因为服务没有第三方依赖。用 Chrome 打开 http://127.0.0.1:8787,右上角应显示“共享服务”。A 账号注册并发布,B 在另一浏览器或无痕窗口中查找,A 更新结果后,B 刷新即可看到变化。

账号和线索保存在 data/shijian.json,该目录不会上传 GitHub。重启服务后数据保留,登录会话需要重新建立。当前只支持单个服务进程,适合教学演示;还没有实时推送、公开生产部署或多实例支持。GitHub Pages 本身只提供静态页面,不能运行这个后端。

十、Git 提交和遇到的问题

1. 分阶段提交

主仓库当前按功能保存了以下提交,可在提交历史查看对应差异。

提交 主要内容
6fae3d5 业务校验、本机持久保存和单元测试
419cd82 共享服务、发布者权限与接口测试
d1579b3 线索墙、发布、详情、我的发布与页面交互
f0d9bdb 明确测试入口,避免不同 shell 的通配符差异
d2b9902 运行文档、白盒测试报告、图示与自动测试 / 部署流程
1e855fe 修正损坏本机记录的备份,增加原始数据导出断言
c053fa7 用钥匙、雨伞、耳机等物品示意图替换单字图标
742fec7 修正与作业仓库命名不一致的链接
4e9d0c2(个人 fork) 补文件存储失败恢复测试,更新测试入口与报告
669c4b8 合并 PR #1,纳入文件存储测试、报告和走查截图

GitHub 分阶段提交记录

另一位同学的 fork、本次提交和 PR #1已经可查。上面的截图记录主仓库先前提交。

黄锦培个人 fork 的测试提交与差异

上图是 GitHub 上 4e9d0c2 提交页的实际截图。下面是 PR #1 开放时的历史截图;该 PR 后来已合并,结果可在 PR 页面核对。

黄锦培 fork 向主仓库提交的 PR #1

合并后的 GitHub 页面显示 PR 已合并到主仓库,合并提交为 669c4b8,两项检查通过。合并截图保存在个人 fork 的 docs/images/。

PR #1 合并完成

2. 草稿清空了,为什么再次进入还会恢复

第一次处理是发布成功后调用 clearDraft(),看起来已经结束。但跳转详情会触发 hashchange,旧表单仍在页面上,离开页面时的草稿保存又把它写回。最终在成功发布时增加一次性跳过标志,并保留普通离开时的保存行为。复核方式是发布后再次进入发布页,确认标题为空、旧草稿没有恢复。

3. 两种保存方式不能让用户混淆

单纯使用浏览器存储可以解决刷新丢失,但不能让另一台设备自动看到同一条线索。程序因此提供共享服务,同时在页面右上角写出当前模式,并在首页说明数据范围。在线体验链接没有部署后端,就明确写成本机体验;完整的账号共享流程按 README 启动服务演示。

4. 按钮隐藏不能代替权限判断

如果只在前端隐藏“标记已归还”,别人仍可能直接请求接口。共享服务根据会话取得用户身份,再调用状态变更规则。接口测试专门注册第二个账号,尝试修改第一个账号的线索,确认返回 403;浏览器中也检查了第二个账号没有修改入口。

5. 导出备份也要检查是否包含待恢复的数据

本机记录损坏时,程序会保留原值并进入临时模式。复核发现,如果导出按钮只保存临时的示例数据,这份备份仍然无法用于检查原来的问题。后来在异常情况下附加 recoveryOriginalRaw,把无法解析的原始字符串一起导出;正常备份不会多出这个字段。存储测试同时检查原值没有被覆盖、导出包含原字符串。这个问题说明,错误提示中的每一步建议都应该有实际可用的操作对应。

十一、个人总结

作为大三计算机科学与技术专业的学生,我对这次题目的理解比“把原型里的页面做出来”更具体了一些。一个页面可以有发布按钮、成功提示和漂亮的卡片,但这些东西之间还需要由同一份数据连接。刷新后能不能找到刚才的记录,换个账号能不能看到处理结果,都是使用流程的一部分。

这次实现最值得整理成经验的是状态和归属。寻物完成叫“已找到”,招领完成叫“已归还”,两种文字看似只是显示差异,底层却需要统一的状态规则。列表、详情和“我的发布”如果各写一套判断,后续很容易有一个地方漏改。把状态规则放到独立模块,再让页面读取同一结果,理解起来也更清楚。

另一个值得注意的地方是错误提示。表单缺少地点时,用户需要知道该补哪个字段;保存空间不足时,用户需要知道本次没有保存;还未完成的文字则需要留住,不能因为一处错误让人重写整张表单。这些情况不会增加很多页面,却会直接影响程序是否顺手。草稿在跳转事件中被再次写回的问题,也提醒我检查操作之间的顺序,不能只看某一个函数是否执行过。

测试部分让我更在意断言的对象。只检查“没有报错”太宽泛,还应检查记录是否新增、原数据有没有改变、状态是否正确、错误是否对应预期分支。比如配额不足的测试同时检查失败和旧列表不变,权限测试检查另一个账号收到 403。这样才能说明程序为什么符合规则。

在界面上,我希望继续保留校园线索卡的特点,让时间和地点固定在容易扫到的位置。以后如果继续做这个项目,会先考虑编辑已有线索、明确的数据整理方式和更完整的账号恢复,再决定是否增加复杂功能。本轮已用 Chrome 检查完整 ZIP 的本机流程和共享双账号流程;Chrome 中的日常操作体验仍需本人复核,未逐项实时计时的个人耗时也只能回顾估算。

十二、对结对同学的评价

  • 值得学习:黄锦培在个人 fork 中把文件损坏保护、事务失败后的恢复写成可重复运行的测试,并通过 PR 提供差异和测试结果。这种用失败路径验证数据不丢失的做法值得保留。
  • 改进建议:后续可以更早提交小范围 PR,让主仓库维护者在实现期间就能审查边界用例;每次复核也应在 PR 中写下运行命令和结论。这里评价的是可见提交方式,线下合作细节不从仓库记录推断。

附录:业务模块的白盒用例

编号 对应判断或路径 测试数据与预期
C01 有效寻物与 trim 标题带首尾空格;规范化并显示寻找中
C02 招领的初始标签 found+open;显示招领中
C03 七项必填校验 逐项置空;每项返回对应错误
C04 空格输入校验 空格、换行、全角空格;不能当有效字段
C05 标题上限 40 字通过、41 字拒绝
C06 时间解析和未来判断 无效日期、晚一天;返回时间错误
C07 图片格式与长度 SVG、超长数据拒绝;允许格式通过格式门槛
C08 多词共同匹配 usb 黑色;仅返回同时匹配的记录
C09 空搜索和无结果 空白返回全部;火星飞船返回空数组
C10 条件组合 found+图书馆+证件卡片+open;只有 sample-001
C11 时间排序 返回降序,原数组顺序不变
C12 合法状态转移 本人完成两类线索,再重新开放
C13 归属失败 其他人、空身份、null;均拒绝
C14 记录或状态无效 缺失记录 404;deleted 状态拒绝
C15 重复完成 不改变 updatedAt
C16 输出转义 img 标签和 onerror 引号转为文本
C17 账号校验 账号规范化;无效账号组合拒绝
C18 初始数据结构 九条示例全部通过结构检查

本机存储和接口测试的完整表格、代码均在仓库中。覆盖尚未包含真实磁盘写满、大量账号和所有页面异步情况,后续可以从这些边界继续补充。

参考资料

posted @ 2026-10-10 23:05  Grim761  阅读(5)  评论(0)    收藏  举报