结对作业2:校园失物招领小程序——核心功能实现

结对作业2:校园失物招领小程序——核心功能实现

软件工程与实践 · 结对作业(2 人)· 2026-10-07 ~ 2026-10-09

这个作业属于哪个课程 2026-01软件工程与软件工程实践
这个作业的要求在哪里 结对作业二:核心功能实现
我的博客 https://www.cnblogs.com/zzq1419
结对同学的博客 https://www.cnblogs.com/zkk234
结对的 GitHub 仓库 https://github.com/zzq1419/102402150-102402148
结对成员 102402150 周子钦、102402148 周凯

〇、和第一次作业的关系

第一次作业做完的是需求分析 + Figma 原型(7 个画框、18 条连线),这次把它落成能跑的程序:
7 个页面互通、数据能增删改、搜索与四重筛选真的能筛、发布带校验、状态能更新。
有几处是故意改掉的,免得看起来像漏做:

差异 原型 实现版 原因
手机状态栏 有 没有 原型是 App 截图,实现是网页
搜索筛选 3 个胶囊 4 个下拉框(多了"分类") 多一维更好用,四项一行放不下
联系方式 直接显示 打码,点击复制才给完整 防爬虫批量抓号码
图片上传 占位方块 真能选图、本地压缩 原型只是示意
「管理账号」入口 有 去掉 原型里是没连页面的占位

一、具体分工

成员 负责内容
102402150 周子钦 整体架构与分层设计、utils.js 纯函数层、app.js 路由与事件、单元测试、README 与博客
102402148 周凯 主干流程与页面跳转关系核对、「我的发布」模块;复审后补齐三处"只做了一半"的功能(收藏入口 / 卡片缩略图 / 联系方式预填)并补了 10 条用例,经 Fork + Pull Request 合入主干

两个人不是各写一半再拼,而是先一起把数据结构和分层定死,再分头实现、互相复审。
他提的 PR 我逐行看过才合并,复审时还发现了一个问题(见困难 3)。


二、PSP 表格

PSP2.1 阶段 预估 实际
Planning 计划 15 15
· Estimate 估计这个任务需要多少时间 15 15
Development 开发 335 445
· Analysis 需求分析(包括学习新技术、复习作业 1 原型) 30 35
· Design Spec 生成设计文档 25 30
· Design Review 设计复审 15 20
· Coding Standard 代码规范(为目前的开发制定合适的规范) 10 15
· Design 具体设计(分层、数据结构、路由) 40 45
· Coding 具体编码 150 200
· Code Review 代码复审 25 40
· Test 测试(自我测试,修改代码,提交修改) 40 60
Reporting 报告 40 40
· Test Report 测试报告 15 15
· Size Measurement 计算工作量 5 5
· Postmortem 事后总结,并提出过程改进计划 20 20
合计 390 500

差距最大的是"具体编码"(150 → 200):一是那几个 bug 都属于"代码看着对、跑起来不对",
光定位就花了近 40 分钟——预估只算了"写出来要多久",没算"确认它真的对"要多久;
二是复审时又挑出三处"功能只做了一半"的地方,补完还加了 10 条用例。


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

3.1 整体思路

两个问题决定了一切:数据存哪、代码怎么分。不允许后端,数据就只能存浏览器本地
(localStorage,刷新不丢)。更关键的是第二个:

把"不碰 DOM 的纯函数"单独抽成一层。​

单元测试最怕"逻辑和界面缠在一起、不开浏览器就测不了"。把搜索、筛选、校验、格式化这些
输入决定输出的逻辑全放进 utils.js,它们就成了能在 Node 里直接跑的普通函数,
测试不用浏览器、不用模拟点击,跑一遍几十毫秒。

于是有了五层(图 1):数据层 data.js(只读常量)、纯函数层 utils.js(不碰 DOM 和存储)、
仓库层 store.js(增删改查 + 持久化)、视图层 ui.js(数据 → HTML 字符串)、
控制层 app.js(唯一碰 DOM 的地方,hash 路由 + 事件委托)。

为了"双击 index.html 就能跑",没用 ES Module、没用任何框架(file:// 下 import 会被 CORS 拦):
每个文件做一层环境判断,Node 里走 module.exports、浏览器里挂 window——
同一份 utils.js 既被网页加载,也能被 node tests/unit.test.js 直接 require。

3.2 关键实现的数据流图

图1-数据流图

用户点一下之后,app.js 解析 hash 决定显示哪页,从 store.js 取数据,交给 utils.js
筛选/校验/格式化,最后让 ui.js 拼成 HTML 写回 DOM。整条链路上 utils.js 和 store.js
都不碰 DOM
——这就是它们能被单独测试的原因。

路由用 hash 路由(#/home、#/detail/LF1001):hash 变化不会请求服务器,
所以 file:// 双击打开也能跳转,刷新还能停在当前页。

真正踩坑的是返回。第一版所有返回按钮都写死 href="#/home",从详情页点返回一律弹回首页
——搜到一半点开一条,返回一下搜索结果全丢。改成站内返回栈才对(图 2):

图2-返回栈

3.3 关键代码片段

(1)返回栈的入栈规则

/* js/utils.js */
function pushRoute(stack, hash) {
  var s = (stack || []).slice();
  var h = String(hash == null ? '' : hash);
  if (!h) return s;

  var top = s[s.length - 1];
  if (top === h) return s;                                    // 1 同页重渲染 → 不动
  if (s.length >= 2 && s[s.length - 2] === h) {               // 2 浏览器后退 → 出栈
    s.pop(); return s;
  }
  if (top && routePath(top) === routePath(h)) {               // 3 同页参数变化 → 原地替换
    s[s.length - 1] = h; return s;
  }
  var seen = s.indexOf(h);
  if (seen > -1) return s.slice(0, seen + 1);                 // 4 回到访问过的页面 → 截断
  s.push(h);                                                  // 5 新页面 → 入栈
  return s;
}

为什么贴这段:它要是直接写在 app.js 里操作 history,就只能靠手点验证;抽成纯函数后,
"连点两次返回停在哪"变成可自动断言的用例。第 3 条规则还是测试时才发现要补的——
一开始只有"新页面入栈",于是在搜索页里改筛选也算新页面,筛三次要点三次返回才出得去。

(2)搜索高亮:先切分原文,再逐段转义

/* js/utils.js */
function highlight(text, q) {
  var raw = String(text == null ? '' : text);
  var kws = parseKeywords(q);                    // trim → 小写 → 按空白切 → 去重
  if (!kws.length) return escapeHtml(raw);

  var re = new RegExp(kws.map(escapeReg).join('|'), 'gi');
  var out = '', last = 0, m;
  while ((m = re.exec(raw)) !== null) {
    /* 未命中部分原样转义;命中部分转义后再包 <mark> */
    out += escapeHtml(raw.slice(last, m.index)) + '<mark>' + escapeHtml(m[0]) + '</mark>';
    last = m.index + m[0].length;
  }
  return out + escapeHtml(raw.slice(last));
}

为什么不直接在转义后的字符串上替换:关键词含 &、< 时会对不上,而且替换出来的 <mark>
本身会被后续关键词命中、拼出嵌套标签。先在原文上切分、再逐段转义,两个问题就都没了。

3.4 数据结构

{
  id: 'LF1013',       // 取现有最大编号 +1,删掉中间项也不会重号
  type: 'found',      // found=失物招领 / lost=寻物启事
  title: '测试钥匙串', cat: '钥匙',
  area: '教学楼',     // 由 place 自动推断,供"地点"筛选使用
  place: '实验楼 5 楼机房', timeText: '2026-10-08 10:00',
  contact: '13912345678', desc: '……',
  images: [], status: 'open', owner: true,   // 图(最多 3 张)/ 状态 / 是不是我发的
  createdAt: 1759900000000
}

两个细节:area 由 place 自动推断(areaOfPlace('第二食堂二楼') 得出 '食堂'),
用户不用多填字段、地点筛选又能命中;示例数据的发布时间是"相对当前时间"生成的,
否则过几天打开,"24 小时内"会什么都筛不出来。


四、附加特点设计与展示

4.1 创意与意义

作业正文点名的两个方向(按类别/地点筛选、一键复制联系方式)都做了,另外加了六个:

  1. 联系方式默认打码,只显示 138****8000,点「一键复制」才给完整值——失物招领里是真实手机号,
    直接摊开显示,一条爬虫就能抓走全校号码。
  2. 已归还 / 已找到的信息自动从首页隐藏——对应作业正文里"减少重复询问和无效联系"。
  3. 图片在浏览器本地压缩(canvas 等比缩到最长边 640px 存 dataURL),全程不联网、不上传服务器。
  4. 收藏了的东西找得回来。​ 星标能收藏,但收藏完没地方看就等于没做,所以在「我的发布」加了
    「收藏 N」 分段——不受"我发布的"限制,别人发的那条也能在这儿找到。
  5. 列表卡片显示物品缩略图:有图片用第一张,没图片才退回分类图标。
    (示例数据不带图片,所以双击打开看到的仍是图标,上传过图片的条目才显示缩略图。)
  6. 发布页预填自己的联系方式,不用每次重打。

4.2 实现思路

特点 实现思路
联系方式打码 详情页判断 owner,不是自己发的走 maskContact();点复制时从 store 取完整值写剪贴板,页面上始终不出现完整号码
状态联动隐藏 首页查询固定带 status: 'open',标记完成后自然不满足条件;状态可逆
图片本地压缩 FileReader → Image 取尺寸 → 按最长边 640px 缩放画到 canvas → toDataURL('image/jpeg', 0.7);带 8MB 上限与"不是图片"的兜底
一键复制 优先 navigator.clipboard,失败退回 execCommand('copy')(file:// 下前者可能不可用),都失败则提示手动长按
四重筛选 / 校验 筛选全走 filterItems() 取交集,状态存进 URL(刷新不丢、能分享);校验与界面分离,返回 {ok, errors},界面只负责标红并滚到第一条错误
收藏入口 收藏只是一张「编号 → 1」的表;列表取收藏编号与现有信息的交集,所以条目被删除后不会留下点不开的幽灵项,计数也不虚高
卡片缩略图 缩略图函数改成吃整个 item,有 images[0] 就渲染 <img>、否则回退图标;四个列表共用同一个函数,规则只有一处
联系方式预填 把 DATA.ME.contact 写进表单 value;并加测试兜底——预填值本身必须合法,否则预填反而会挡住发布

4.3 关键代码片段

(1)图片本地压缩("不联网也能用"的关键)

/* js/app.js */
function readImageFile(file, cb) {
  if (!/^image\//.test(file.type)) { cb(null, '请选择图片文件'); return; }
  if (file.size > 8 * 1024 * 1024) { cb(null, '图片太大了,请选 8MB 以内的'); return; }

  var reader = new FileReader();
  reader.onload = function () {
    var img = new Image();
    img.onload = function () {
      var MAX = 640;                                 // 最长边压到 640px
      var k = Math.min(1, MAX / Math.max(img.width, img.height));
      var cv = document.createElement('canvas');
      cv.width = Math.round(img.width * k);
      cv.height = Math.round(img.height * k);
      cv.getContext('2d').drawImage(img, 0, 0, cv.width, cv.height);
      cb(cv.toDataURL('image/jpeg', 0.7));           // 压缩后才是 dataURL
    };
    img.src = reader.result;
  };
  reader.readAsDataURL(file);
}

(2)卡片缩略图:有图用图、无图回退图标

/* js/ui.js —— 参数兼容 thumb(item) 和 thumb('水杯') 两种写法 */
function thumb(it) {
  var item = (typeof it === 'string') ? { cat: it } : (it || {});
  var imgs = item.images || [];
  if (imgs.length) {
    return '<div class="thumb thumb-img"><img src="' + E(imgs[0]) + '"></div>';
  }
  var cat = item.cat;   /* 没有图片才走原来那套分类图标 */
  ...
}

为什么值得贴:它把"发布时传的图"和"列表上能看见"接上了——之前这两件事是断开的
(图存进去了、详情页也显示,唯独列表不显示),属于功能做了一半。它同时修掉一个安全问题:
图片地址是用户可控的字符串,拼进 src 前必须走 E() 转义,否则
x" onerror="alert(1) 这种能撑出属性、执行脚本。

(3)收藏列表:用"交集"避免幽灵条目。​ 收藏表只存编号,列表是拿现有信息去过滤
(state.items.filter(it => favs[it.id]))。反过来直接遍历收藏表的话,条目被删掉之后编号还在表里,
页面就会出现点不开的卡片、计数也虚高。

4.4 实现成果展示

首页(分类筛选 + 悬浮发布按钮) 搜索(关键词高亮 + 四重筛选)
01-首页 02-搜索页
发布页 · 寻物(联系方式已预填) 信息详情页(联系方式打码 + 一键复制)
03-发布页 06-详情页
我的发布(分段含「收藏 2」;首条卡片用上了缩略图) 收藏列表(自己收藏的和别人的混在一起排)
04-我的发布 05-收藏列表

底部导航中间凸出栏顶的"发布"按钮做成凸起,是为了让最常用的操作有更大的点击区域
(直径 44px,符合移动端最小舒适点击区),下半截仍嵌在导航栏里、不显得突兀。


五、目录说明与使用说明

5.1 目录组织

102402150-102402148/          ← 仓库根目录 = 项目根目录
├── index.html              入口页。只有骨架:外壳 + 挂载点 + 一条 noscript 提示
├── README.md               目录说明 + 使用说明
├── css/style.css           全部样式。顶部 :root 是设计变量(颜色/圆角/阴影/间距)
├── js/
│   ├── data.js             【数据层】分类、区域、时间片、图标、12 条示例信息
│   ├── utils.js            【纯函数层】搜索筛选、表单校验、时间格式化、返回栈
│   ├── store.js            【仓库层】增删改查 + localStorage 持久化
│   ├── ui.js               【视图层】把数据渲染成 HTML 字符串
│   └── app.js              【控制层】hash 路由 + 事件绑定
├── tests/
│   ├── unit.test.js        81 个单元测试用例(Node 和浏览器都能跑)
│   └── tests.html          浏览器版测试报告页,双击即可查看
├── screenshots/            README 的界面预览图(程序不依赖,删掉也能跑)
└── assets/                 预留的静态资源目录(图标都是内联 SVG,目前为空)

组织原则就一条:按"层"分,不按"页"分。​ 没有 home.js / search.js 这种按页面拆的文件——
7 个页面的渲染共用同一套组件函数,按页面拆会产生大量重复。依赖关系单向,
index.html 里的加载顺序就是依赖顺序:data.js → utils.js → store.js → ui.js → app.js。

5.2 测试人员如何运行

不需要安装任何东西,不需要 Node,不需要本地服务器。​

  1. 打开仓库 https://github.com/zzq1419/102402150-102402148,点 Code → Download ZIP 解压(或 git clone);
  2. 双击解压出来的 index.html,用 Google Chrome 打开(与作业要求一致)——地址栏会出现 ...#/home,页面直接可用。

想跑单元测试:node tests/unit.test.js(结尾打印 共 81 个用例:通过 81,失败 0),
或双击 tests/tests.html 看分组报告页。

双击后若是空白页:确认 css/、js/ 与 index.html 在同一层目录(别被解压工具打散)。


六、单元测试

6.1 用什么工具,以及一份简易教程

没用任何测试框架,自己写了个 20 行的迷你框架。​ 项目零依赖,为写测试去装 Jest 要引入
package.json 和 node_modules,与"双击就能跑"冲突;我们真正需要的只有三件事——
断言、收集结果、打印报告。

var results = [];
function it(name, fn) {
  try { fn(); results.push({ name: name, ok: true }); }
  catch (e) { results.push({ name: name, ok: false, msg: e.message }); }
}
function ok(cond, msg)  { if (!cond) throw new Error(msg || '期望为真'); }
function eq(a, b, msg)  { if (a !== b)
  throw new Error((msg||'') + ':期望 ' + JSON.stringify(b) + ',实际 ' + JSON.stringify(a)); }
function deepEq(a, b, msg) {                 // 比数组/对象必须用它
  if (JSON.stringify(a) !== JSON.stringify(b))
    throw new Error((msg||'') + ':期望 ' + JSON.stringify(b) + ',实际 ' + JSON.stringify(a));
}

两个必须知道的坑: ① 比数组千万别用 eq——eq([1],[1]) 永远失败(!== 比的是引用),
我们第一版有 6 个用例就这么"红"了,报错还长得一模一样。② 涉及时间的函数要把"当前时间"
做成参数
,测试时注入固定值,否则用例今天过、明天挂。
还有个判断"测试有没有生效"的办法:把已修好的 bug 故意改回去,看用例会不会变红。

6.2 部分测试代码与说明

it('[筛选] 按时间片筛选,边界按"距今多久"算', function () {
  // 夹具五条数据的 createdAt 分别是 NOW-1h / -20h / -2天 / -6天 / -3h
  eq(UTILS.filterItems(FIXTURE, { range: 'h24', now: NOW }).length, 3);
  eq(UTILS.filterItems(FIXTURE, { range: 'd3',  now: NOW }).length, 4);
  eq(UTILS.filterItems(FIXTURE, { range: 'd7',  now: NOW }).length, 5);
});

it('[缩略图] 有图片显示第一张图,没有图片才回退分类图标', function () {
  var MARK = '<rect x="3" y="5.5"';            // 分类图标(内联 SVG)的开头几个字符
  var withImg = UI.cardItem(mk({ images: ['data:image/jpeg;base64,AAAA'] }), '', false);
  ok(withImg.indexOf('<img src="data:image/jpeg;base64,AAAA"') > -1, '有图片时应当渲染缩略图');
  eq(withImg.indexOf(MARK), -1, '有图片时不该再画分类图标');
  ok(UI.cardItem(mk({ images: [] }), '', false).indexOf(MARK) > -1, '无图时应当回退图标');
});

缩略图那条用例用的是白盒思路:不断言"看起来对不对",而是断言"产物里有没有某段 HTML"——
分类图标是一段内联 SVG,取它开头的 <rect x="3" y="5.5" 当指纹,就能判断"到底画了图标还是图片"。
这比截图比对稳,换个颜色也不会误报。

81 个用例的分组:

分组 数 覆盖什么
关键词解析 4 空白切分、大小写、去重、空输入容错、多词 AND
筛选 filterItems 13 六维筛选、条件交集、三种排序、纯函数不被污染
高亮与 HTML 安全 5 转义、高亮、XSS 不生效、正则特殊字符
发布表单校验 9 必填、长度上下界、分类白名单、联系方式三种格式
时间格式化 5 刚刚/分钟前/今天/昨天/跨年、补零、未来时间兜底
业务小工具 7 地点→区域推断、统计、状态翻转、编号自增不重号
视图渲染冒烟 17 9 个视图产物扫描 + 打码与按钮随身份切换 + 防注入
返回栈 pushRoute 11 五条入栈规则 + 两条完整链路
收藏 / 缩略图 / 预填 10 收藏计数与列表、已完成条目仍列出、编号被删不出幽灵条目、有图显示图片 / 无图回退图标、四个列表共用同一套规则、图片地址引号被转义、两个发布页都预填且预填值合法

6.3 测试数据怎么构造,怎么防"刁难"

不用真实的 12 条数据,而是另造 5 条、每条负责一个测试维度:

夹具 类型 分类 区域 状态 距今 负责验证
LF9001 found 证件卡类 教学楼 open 1 小时 关键词命中、最新排序
LF9002 found 雨伞 图书馆 open 20 小时 分类/区域筛选、24 小时边界
LF9003 lost 电子设备 食堂 open 2 天 类型筛选、三天边界
LF9004 lost 水杯 运动场 done 6 天 状态筛选、已完成排最后、七天边界
LF9005 lost 钥匙 宿舍楼 open 3 小时 mineOnly

真实数据每次改动都会让一堆用例跟着红;夹具是为测试而生的,改起来不会互相牵连。
边界要取"刚好在内"和"刚好在外"​:上面那条时间筛选用例让 24 小时 / 三天 / 一周恰好命中
3 / 4 / 5 条——只测"筛出来有东西"的话,阈值写成 < 还是 <= 根本测不出来。

把能想到的"刁难"提前堵上:

假想的刁难 应对
搜索框输入 <script>alert(1)</script> 或正则特殊字符 a+b 断言标签被转义;escapeReg 转义后正常匹配
搜索框全空格 / 什么都不填 / 条件互相矛盾 返回空数组,不报错也不返回全部,界面走空状态
联系方式填 12 位、或 12800138000;名称只填 1 或 31 个字;连点两次提交 判非法 / 上下界都拦 / 每次生成新编号不重号
收藏过的那条后来被删了(或点了「恢复示例数据」) 收藏栏取"编号交集",该条自动落空,不留点不开的卡片、计数也不虚高

够不够?说实话:能覆盖"业务逻辑正确",覆盖不了"体验正确"。​
返回该退到哪一页是链路问题、文案有没有拼成 NaN 是产物问题、"功能是不是只做了一半"是完整性问题——
下一节的三个困难没有一个是最初那 70 条用例发现的,全是真机走查和看截图时才露出来,
之后才补测试把它们钉住。


七、GitHub 代码签入记录

共 17 次提交,按"一个功能一次提交"划分:前 12 次是主线开发(10-07 ~ 10-08),
第 13~15 次是周凯 Fork 后的补齐(10-09),第 16 次合并他的 PR,第 17 次合并后同步 README 配图。

签入记录

提交 说明
chore: 添加 .gitignore… / chore: 初始化项目骨架… 忽略临时文件;入口页 + 资源目录
feat: 定义设计变量与全局样式… / feat: 加入分类、区域、图标常量与示例数据 样式层与数据层
feat: 实现纯函数层… / feat: 实现数据仓库与 localStorage 持久化… 纯函数层、仓库层
feat: 实现 7 个页面的视图渲染… / feat: 接入 hash 路由、事件委托… 视图层、控制层
test: 加入 70 个单元测试用例… / test: 加入浏览器版测试报告页 单元测试与测试报告页
docs: 编写 README… / docs: README 增加「界面预览」… 文档与仓库配图
feat: 补齐三处做了一半的功能… / test: 补 10 条用例… / docs: README 同步…(周凯) 收藏 / 缩略图 / 预填;用例 70 → 81
Merge pull request #1 from zkk123-dd/… / docs: 重出界面预览截图…(我) 复审后合并、同步配图

提交信息统一用 类型: 做了什么(chore / feat / test / docs),每个提交都能独立说明一件事。
协作按作业要求走:我建仓库 → 周凯 Fork 后在自己分支上提交 → 开 PR 合回主干(PR #1),
他那一支正好是"改功能 / 补测试 / 同步文档"三件事。


八、遇到的代码模块异常与结对困难

困难 1:发布成功页的文案变成了「NaN」

  • 问题:成功页写的是"把状态改成「NaN」",本该是"已归还/已找到",而且控制台一个报错都没有。
  • 尝试与解决:先查数据源(正常)→ 把文案单独 console.log(结果是好的,说明问题在
    拼接过程)→ 拎出来看,行尾一个 +、下一行行首又一个 +,两个 + 撞成一元加号
    ('…「' + (+'已归还')),作用在字符串上就得到 NaN。
  • 收获:单测只覆盖 utils.js、测不到视图层,而视图层全是字符串拼接。于是补了
    「视图渲染冒烟测试」(9 个视图全渲染,断言产物不含 NaN/undefined/[object Object])
    并把 bug 改回去验证用例真会变红。​"不报错的 bug"比"报错的 bug"危险。

困难 2:左上角返回不管从哪进都弹回首页

  • 问题:返回按钮都写成 href="#/home",从详情页点返回不管从哪进来一律回首页。
  • 尝试与解决:想用 history.back(),但 file:// 直接打开某页时站内没历史,一点就跳出站点;
    改成自己维护站内返回栈。​还补了一条漏掉的规则——第一版只有"新页面入栈",于是
    在搜索页里改筛选也入栈,筛三次要点三次返回(端到端验证时点出来的)。
  • 收获:规则抽成纯函数 pushRoute() 后,"连点两次返回停在哪"变成可自动断言的用例。
    判断一段逻辑该不该抽成纯函数,标准是"它能不能被断言"。​

困难 3:三处"功能只做了一半"的地方

  • 问题:走查时发现三处能点、不报错、但没做完的地方——① 详情页能收藏,但
    全站找不到已收藏的列表;② 发布时能传图、详情页也显示,但列表卡片永远只画分类图标;
    ③ DATA.ME.contact 早就定义好了,发布表单却是空的。都"功能在",只是差最后一步。
  • 尝试与解决:交给周凯在他的 fork 里补齐(第 13 个提交),我复审后合并。复审时又发现一个隐患:
    收藏列表若直接遍历收藏表,条目被删除后会留下点不开的卡片、计数还虚高;
    改成"拿现有信息去查收藏表"就自动落空,并补了用例钉住。
  • 收获: “做完了"和"能用"是两件事。​ 一个功能要算完成,得把”** 入口 → 操作 → 结果**"三段走通:
    能收藏只是中段,还得知道去哪看、看到什么。我们把这三处写成 10 条用例,
    用测试来定义"什么叫做完"。​

结对中的一次分歧:校验提示放哪

周凯主张点「发布」时统一弹提示、一次列全;我主张输入框下方即时提示。争到一半发现,两个人说的
其实是两个场景——前者解决"我填完了要提交",后者解决"我正在填、不确定对不对",最后两个都做了。
收获:分歧往往不是"对错"之争,而是两个人想的是不同场景。


九、评价我的队友(周凯)

值得学习:

  1. 对"用户在哪一步会卡住"特别敏感。​ 第八节那次分歧就是例子——他指出"填的时候不知道对不对,
    用户会来回试",回头看他是对的。这类判断靠的不是技术,是真的站在用户角度想了一遍。​
  2. 会盯着"没做完的地方"看。​ 困难 3 那三处都是他走查挑出来的。我最缺的就是这种习惯:
    我习惯确认"功能在",他习惯确认"功能能用"。
  3. 流程意识强,先用 fork + pull request 提交改动而不是直接推主干,少了很多返工。

需要改进:

  1. 提交是攒齐之后一次性推上来的(那一支三个提交时间戳相同),PR 里看不到中间过程;
    如果能改一点推一点,评审时更容易跟上思路。
  2. 对前端调试手段还不熟,遇到"宽度差一截"习惯盯着代码看,其实开发者工具量一下就知道。

本文使用博客园 Markdown 排版。

posted on 2026-10-09 11:17  Tables  阅读(5)  评论(0)    收藏  举报