软件工程第四次作业

项目 内容
这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering
这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/202601SofwareEngineering/homework/16744
这个作业的目标 通过完善与实现原型掌握结对编程
学号 052403137,182400320
结对成员博客 cnblogs/155TuT,cnblogs/mukyng
项目地址 052403137-182400320
技术选型 React + Vite
Release 拾伴 v1.0.0

0x00 对第三次作业的反思

对自己做的好的地方的思考

我觉得上一次做得比较好的是我们的思考没有停在功能清单上。第三次作业中,我们从第一版的发布、搜索和详情,继续追问信息从哪里来、拾物者是否方便保管、浏览者为什么愿意提供线索。因此第二版加入短表单、可选图片、来源说明和协助反馈,并保留建议错误时人工修改、继续发布的入口。这些考虑让功能有了对应的使用情境,也提醒着我:方便用户,要落实到少填什么、哪里可以退出、失败后怎样继续。

我们也尝试把联系之后的事情讲清楚。prd区分发布者、持有人、认领者和服务点,写出待确认、已核实完成与本人自报找回的区别,再把进展放回“我的事项”。服务点接收并不证明归还,单方确认也不算作核实找回。虽然这些仍是设计,但至少让我们开始考虑谁负责推进、谁来确认结果,而不是把点击“联系”当作流程结束。

另一个收获是真正体会到了原型设计的优劣,我们愿意回到原型中查问题。交付记录保留了历史点击走查,后续流程检查又记录了保存草稿后返回、提交后回退、重复搜索和筛选条件丢失等问题及修复,其中 8 条关键路径都有实际的点击记录。把连线检查与点击结果分开记录,比只展示页面更能说明问题;同时明确静态状态页的限制,也能避免把原型演示写成已经完成的程序能力。

做的不够好的地方的反思

  1. 范围还是铺得太开。 我们虽然在 PRD 中划分了阶段,也排除了地图和完整即时聊天,但仍展开了服务点、审核、奖励与申诉等原型不必要考虑的方面,导致原型最终有 116 个手机画板。考虑完整产品有价值,却不能代替两个人在本次作业中的取舍。这次应先把发布、浏览或搜索、查看详情、联系发布者和更新状态做扎实,再按时间与必要性决定扩展功能。

  2. 需求缺少真实用户反馈。 上次博客写了“调研”,但现有材料主要是题目、公开资料、我们自己的讨论和设计推演,没有具体的用户访谈或试用反馈记录。短表单是否真的省事、核验是否过重、服务点是否愿意承接,目前都还是假设。后续应请真正丢过或捡过物品的同学独立完成任务,记录卡住的位置和原因;涉及校内运营的安排,还要向实际负责人确认。没有这些证据,就不能声称提高了找回率或缩短了寻物时间。

  3. 复杂流程掩盖了最基本的状态维护。 我们设计了核验和交接,但本次要求的“由发布者标记已找到或已归还”还需要在实现与博客中直接说明:发布者从“我的发布”进入本人记录并更新状态,其他用户在列表和详情看到相同结果。本人自报与核实归还的区别可以保留,却不能让简单结案依赖尚未接入的服务点或审核人员。后续要验证操作权限、更新后的展示,以及重新打开记录时能否读到最新状态。

  4. 交互验证仍然不够完整。 re.md 的全量连线覆盖证明节点和目标有记录,8 条点击路径只证明对应场景走通,并不代表全部分支可用。预置关键词与有限筛选组合,也没有验证自由输入、真实提交和跨页面状态持久化。进入程序实现后,还要检查必填信息缺失、搜索无结果、重复提交、取消后返回等情况,并记录实际结果;流程图可解析、原型能跳转和程序功能通过测试的不同情况需要分别说明。

0x01 PSP

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划(小计) 30 60
Estimate 估计这个任务需要多少时间 30 60
Development 开发(小计) 900 1190
Analysis 需求分析(包括学习新技术) 75 60
Design Spec 生成设计文档 45 60
Design Review 设计复审 30 10
Coding Standard 代码规范(为目前的开发制定合适的规范) 15 10
Design 具体设计 75 300
Coding 具体编码 420 600
Code Review 代码复审 60 30
Test 测试(自我测试,修改代码,提交修改) 180 120
Reporting 报告(小计) 150 95
Test Report 测试报告 90 30
Size Measurement 计算工作量 15 5
Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 45 60
合计 1080 1345

0x02 实现

从原型到程序

这次我们把“拾伴”的实现重点收回到题目要求的基本流程:发布寻物或招领信息,浏览、搜索并查看详情,通过公开联系方式自行联系,最后由发布者更新状态。原型复盘中暴露的草稿返回、重复搜索和条件丢失问题,也成为程序设计时需要处理的具体问题。

程序使用 React + Vite。React 负责根据当前记录更新页面,Vite 负责开发预览和打包。首页、搜索和“我的发布”共用 App.jsx 中的数据;详情只保存当前记录的编号,每次从同一份记录中读取内容。因此,发布或结案后,各入口可以显示一致的结果,不需要分别修改几个页面的文案。

核心流程

flowchart TD A[选择寻物或招领] --> B[填写物品信息和公开联系方式] B --> C{发布字段校验通过?} C -->|否| B1[定位错误并保留已填内容] B1 --> B C -->|是| D[公开预览] D -->|返回修改| B D -->|确认提交| E{写入本地存储成功?} E -->|否| E1[提示失败并保留表单] E1 --> D E -->|是| F[更新统一记录并移除对应草稿] F --> H[查看详情和公开联系方式] F -.->|同步展示| G[首页或搜索结果] G -->|打开记录| H H --> I[用户自行联系并核对物品] I --> J[发布者从我的发布进入本人记录] J --> K{确认结案?} K -->|取消| H K -->|确认| L[检查所有者与当前状态] L --> M{校验与保存成功?} M -->|否| N[提示错误并保留原状态] N --> H M -->|是| O[寻物显示已找到;招领显示已归还] O --> P[列表和详情读取更新后的记录]

发布前检查名称、类别、校区、区域和联系方式,日期可留空,但不能填写无效日期或未来日期;图片是可选项。新记录获得独立编号和发布者信息,编辑本人尚未完成的记录则沿用原编号,不额外增加一条发布。搜索直接读取当前记录,将关键词与名称、描述、类别、区域等信息匹配,新发布的内容也能查到。

状态维护放在详情页:发布者从“我的”中的“我发布的”进入记录,点击“标记为已找到”或“标记为已归还”,确认后才保存。其他用户没有修改入口,数据层还会再次检查所有者。这里的结案表示发布者更新了信息,不代表服务点确认或双方完成核验。

关键代码:保存成功后再更新界面

src/App.jsx 将发布、草稿和结案的持久化收在同一个 commit 函数中:

function commit(next) {
  if (initial.error)
    throw new Error('原有本地数据读取失败,已停止覆盖。请换用可用的浏览器存储后重试。');
  const saved = saveState(localStorage, next);
  dataRef.current = saved;
  setData(saved);
  return saved;
}

这个顺序很重要。saveState 会检查数据结构并写入存储,成功后才返回保存结果;若存储空间不足或访问被阻止,它会抛出错误,后面的界面更新不会执行。编辑页保留填写内容并提示失败,详情页也不会提前变成“已归还”。原有数据读取失败时,则停止覆盖,避免用初始示例替换已有内容。

结案也不只靠隐藏按钮限制。src/model.js 中的 completeItem 先检查记录和操作者,再处理重复请求,以下为其中的校验部分:

const item = items.find((record) => record.id === id);
if (!item) throw new Error('这条信息不存在或已被移除。');
if (!actorId || item.ownerId !== actorId) throw new Error('只有发布者本人可以更新这条信息。');
if (item.status === 'completed') return items.slice();
if (item.status !== 'active') throw new Error('这条信息当前不能结案。');

通过检查后,函数返回更新后的记录数组,设置完成时间,并按寻物或招领生成“已找到”或“已归还”文案。已完成记录再次结案不会重复改写完成时间。这里演示的是业务规则;当前用户是固定的“小拾同学”,本地检查不能替代真实系统的服务端权限控制。

目录与运行方法

052403137-182400320/
├─ index.html             # Vite 开发入口
├─ package.json           # 依赖与运行命令
├─ vite.config.js         # 相对资源路径、单页打包与 web 输出配置
├─ src/
│  ├─ main.jsx            # React 入口
│  ├─ App.jsx             # 页面导航、统一记录与保存
│  ├─ Editor.jsx          # 发布表单、图片、草稿与公开预览
│  ├─ Search.jsx          # 关键词输入、结果与空态
│  ├─ Detail.jsx          # 详情、联系方式与结案确认
│  ├─ Activity.jsx        # 本机认领、协助记录与帮助说明
│  ├─ model.js            # 校验、搜索、状态规则与存储
│  ├─ seed.js             # 虚构示例与初始草稿
│  ├─ ui.jsx              # 通用界面组件
│  └─ *.css               # 页面样式
├─ public/assets/         # SVG、字体与字体许可证
├─ tests/model.test.js    # 数据与业务规则的单元测试
├─ docs/implementation.md # 实现范围及已有验证记录
└─ web/                   # 构建产物,包含 index.html 和 assets/

测试人员下载项目后,保留完整的 web/ 文件夹(已打包为压缩文件),用 Google Chrome 打开 web/index.html。根目录的 index.html 是开发入口,不能与构建后的文件混淆。

需要开发或通过本地服务运行时,安装 Node.js 22.12.0 或更新版本,在项目根目录执行:

npm install
npm run dev

使用 Chrome 打开终端输出的地址。首次安装依赖需要联网;修改代码后,按下面的顺序重新构建并预览交付版本:

npm run build
npm run preview

开始体验时,可以查看初始的蓝伞、水杯和钥匙记录;银色钥匙串属于当前演示用户,可用于练习编辑和结案。发布一条自己的记录后,再搜索该名称、查看详情并更新状态,即可走完基本流程。数据只在当前浏览器、当前打开地址下保存,开发地址、预览地址与直接打开文件之间不会自动共享。

0x03 特点

我们把附加设计放在填写、查找和联系时容易中断的地方。

草稿与公开预览:允许没写完,也允许再检查一次

捡到东西时不一定方便立即填写所有信息,因此表单允许在必填项未补全时手动保存草稿。草稿独立存放,不进入公开列表;从“我的”的草稿箱继续填写时,会恢复已有内容。再次保存沿用同一草稿编号,成功发布后移除对应草稿,避免草稿箱里留下一份已经发布的旧内容。

src/model.js 的 upsertDraft 根据编号决定替换还是新增,关键部分如下;updated 是合并旧内容与本次输入、校验图片后得到的草稿:

return previous
  ? drafts.map((record) => (record.id === id ? updated : record))
  : [updated, ...drafts];

公开预览则让用户在提交前看清物品信息和联系方式,发现不妥可以返回修改。日期不确定可留空、照片不是必填,也让发布不必依赖一份完全齐备的材料。下图展示从钥匙草稿恢复内容后进入公开预览的效果:

草稿箱 公开预览
image image

筛选与条件保留:少重复输入,也能从空结果继续找

除了关键词,程序还提供校区选择,以及“雨伞·图书馆·近 7 天”“钥匙·教学区·今天”“水杯·食堂·近 7 天”等筛选组合。当前界面采用这些预设组合,尚未提供所有条件的自由组合。

实现时,输入框内容 query、已提交的关键词 submitted 和筛选条件 filter 分别保存在 App.jsx 中,结果由当前记录派生。进入详情再返回、按同样关键词重新搜索,都继续使用原有条件;没有结果时,页面提供修改关键词、放宽筛选和发布寻物的入口。筛选浮层先修改临时选项,点击“查看结果”才应用,直接关闭不会误改原条件。

这也回应了原型复盘中的筛选丢失问题。本次打开钥匙结果、应用“钥匙·教学区·今天”、进入详情再返回,关键词和筛选仍然保留。界面中的四个快捷词仅用于填入搜索框,不代表已经实现真实的搜索历史。

image

联系方式复制:把操作结果说准确

详情直接展示发布者愿意公开的联系方式,并提供复制按钮,减少在两个应用之间反复抄写的步骤。src/Detail.jsx 中的实现会等待剪贴板写入成功;如果浏览器不支持或阻止复制,就选中文字,让用户手动复制:

async function copyContact() {
  if (!contact) return;
  try {
    if (!navigator.clipboard?.writeText) throw new Error('Clipboard unavailable');
    await navigator.clipboard.writeText(contact);
    setCopyMessage('联系方式已复制');
    onNotify?.('联系方式已复制');
  } catch {
    selectContact();
    setCopyMessage('未能自动复制,已选中联系方式,请手动复制。');
  }
}

我们希望“复制成功”对应实际结果,失败后也有可继续操作的办法。这里提供的是联系信息与复制便利,实际联系和物品核对仍由用户自行完成。下图为当前演示用户发布的钥匙记录,所以同时显示编辑与结案入口。

image

0x04 测试与issue

这次从测试中整理出 issue,我开始体会到结对开发对问题描述的要求:自己发现了异常,还需要让队友能够复现,知道它影响什么,以及修到什么程度才算完成。仓库中的问题涉及多标签页保存、联系方式校验、草稿状态、分类展示和日期文案等,其中不少都发生在主流程已经能够跑通之后。这也回应了前面原型复盘中的不足:单次操作成功,还需要继续检查操作之间的状态变化。

从代码分支和使用顺序构造测试

项目使用 Node.js 自带的 node:test 与 node:assert/strict,在 tests/*.test.js 中用 test 描述场景、用断言检查实际结果,执行 npm test 即可统一运行。我的理解是,写一个用例应先确定输入和预期,再调用被测函数;遇到失败,要回到业务规则判断实现或预期哪里有问题。

例如,日期测试中有下面这段用例。item 用来构造基础物品记录,now 是固定的测试时间,formatDateLabel 是被测的日期文案函数:

test('missing or malformed eventDate keeps an explicit unknown prompt', () => {
  assert.equal(formatDateLabel(item({ eventDate: '' }), { now }), '时间不确定');
  assert.equal(formatDateLabel(item({ eventDate: undefined }), { now }), '时间不确定');
  assert.equal(formatDateLabel(item({ eventDate: 'not-a-date' }), { now }), '时间不确定');
});

空字符串、缺少字段和非法日期分别对应不同的输入情况;固定时钟还可以检查跨日、跨月和跨年的展示,不需要修改系统时间。这让我对白盒测试有了更具体的理解:沿着校验、状态判断和保存失败等分支设计数据,检查每个分支是否符合预期。联系方式要同时考虑合法输入、纯符号和长度边界;草稿要考虑新建、编辑原记录以及原记录结案后的情况;并发保存则要区分两页修改不同记录和同一记录。测试数据需要对应可能出错的条件,也要检查失败后原内容是否仍然保留。

把 issue 写成双方可以核对的约定

issue #1让我觉得,复现步骤本身就在帮助分析问题。同一地址打开 A、B 两页,A 发布记录,B 不刷新就保存草稿,再检查 A 的记录是否消失,这比一句“本地存储有问题”明确得多。它把影响限定在本地多标签页,并提出同一记录冲突时保留未提交内容的要求,后续 review 就有了可以逐项核对的依据。

issue #2中的 ------- 则是一个很小但有效的反例:输入长度符合原规则,却没有任何可用于联系的数字。问题还涉及发布与认领表单重复维护校验,因此验收也需要检查规则是否共用。写到这一步,我才更清楚地意识到,一个 issue 应当交代问题、复现方式、代码依据和验收标准;建议可以讨论,但用户应得到什么结果需要说清楚。这样的记录能减少队友反复询问上下文,也方便后来的人理解修改原因。

同时,测试记录需要说明证据的范围。issue #8记录了约 7.42 MiB 的字体资源和引用方式,也明确尚未做冷缓存耗时对比;标签交互验收则把 Chrome 中的键盘操作与数据层单元测试分开。这提醒我,报告中“测过了”应当有具体对象和方法:函数断言通过、浏览器操作通过、性能测量完成,各自能够支持的结论不同。

0x05 修复与交付

截至 2026 年 10 月 10 日,仓库的 8 个 issue 中有 6 个已关闭、2 个仍开放;PR #10~#15 已合并。对应关系如下:

问题 修复记录 当前状态
#1 多标签页保存冲突 PR #12,整合时补齐编辑快照与保存保护 已关闭
#2 联系方式校验 PR #13,补充边界测试 已关闭
#3 失效编辑草稿 PR #14,整合时补齐只读查看与丢弃确认 已关闭
#4 分类插画与展示 PR #10 已关闭
#6 标签选中态与键盘导航 PR #15 已关闭
#7 跨日时间文案 PR #9 审查后关闭,后续由 PR #11 合并修复 已关闭
#5 独立组合筛选 尚无已合并的对应修复 PR 仍开放
#8 字体加载优化 尚无已合并的对应修复 PR 仍开放

Review 需要给出可以复查的理由

在 PR #9 的审查中,统一日期计算的方向是合理的,但实际运行时,40 项测试中有 1 项失败:实现返回的文案与测试预期相差一个空格;更直接影响交付的是,源码修改后没有更新 web/index.html,使用交付目录仍会遇到旧问题。这次正式提交了 Request changes,后续修正在 PR #11 中合并。这个过程让我意识到,review 的结论需要由代码位置、失败用例和影响范围支撑,才能让对方知道为什么需要修改、如何验证修改已经完成。

同一次审查还指出,迁移测试直接使用新版本的初始记录,而该记录已经带有迁移后才需要的字段,即使迁移没有正确执行,断言也可能通过。后续用例显式移除了旧数据中不存在的 eventTime,再检查迁移和第二天的展示。这比单纯增加测试数量更值得我学习:测试本身也需要被审查,要确认它确实有能力发现要防止的问题。

按验收标准检查,才能发现修复中的遗漏

PR #12 的 review 记录说明,增加版本号和监听其他标签页更新后,已经打开的旧表单仍可能覆盖同一记录的新内容。整合时继续保留编辑开始时的记录快照,把读取、校验与写入放进同一个跨标签页锁中,并在冲突后保留输入、提供另存草稿的入口。我从中体会到,审查一个修复,需要继续推演原来的失败场景:版本号更新了,用户手里的表单是否也仍然有效?拒绝保存之后,用户又能怎样继续?

PR #14也有类似的遗漏:最初有了失效草稿的丢弃入口,但 issue 还要求用户能够查看保留内容。后续补上只读详情、独立确认和保存失败时保留内容的处理,才把这条流程交代完整。把一个问题拆成独立 PR,能让这些遗漏更容易被发现;审查时回到 issue 的验收标准,也能避免只看新增了哪些代码,就过早判断需求已经完成。

把协作记录和交付结果一起做好

这次流程仍有需要改进的地方。PR #12~#14 的 issue 关联由后续评论补充,review 结论主要记在普通评论中,部分遗漏由我在本地整合时通过 补充提交 2d8c09c修正,再回到 PR 说明结果。这样保留了原有提交和修改依据,但没有完整留下“提出意见—作者逐项回应—正式复审批准”的过程。下一次我希望从创建 PR 时就关联 issue,写清改动范围和验证结果;发现问题后尽量在同一个 PR 中完成回应、补交和复审,让队友也能参与理解修复,看清每次修改的依据。

独立组合筛选和字体优化仍保留在开放 issue 中,后续还需要实现与验证。这次我对规范开源协作的体会,更多落在这些具体记录上:问题为什么提出,修复经过了什么检查,哪些工作已经完成,哪些仍需继续。把这些事情写清楚,队友和后来的维护者才能据此接着做;这也是我以后使用 issue、PR 和 review 时希望坚持的习惯。

0x06 互评

本次我的结对伙伴是林叶鑫,虽然在组队之前没有接触过,但是他的态度很严谨,计划和沟通时很顺畅,分配任务后任务完成的速度和质量也很高;不过他之前没有学习过github流程,我review后的他的pr因与main分支出现冲突(我又合并了一个pr导致文件变更),他没有同步到本地,合并或变基后继续修改,而是直接关闭了pr重新提了一个,这个习惯不太好,需要改进。

posted @ 2026-10-10 04:08  少年漫症候群  阅读(27)  评论(0)    收藏  举报