1.21早就过了零点,但手头的报错还在。屏幕右下角的时间从23:59跳到00:00,我用手指敲了两下空格键,让滚动的日志重新停在一段异常堆栈上。我知道这份实习日志1.21没法在1月21日当天写完——今晚大概率又是个跨越凌晨的加班夜。这个寒假实习说长不长说短不短,但临近年关,很多平时容易一带而过的问题反而集中爆了出来。
今天最深的体会,不是又学了什么新框架、新API,而是突然明白了一件事:把需求文档上的"实现一下"变成线上能稳定跑的功能,中间隔着的根本不是代码量,而是大量具体的、琐碎到几乎不想承认的边界判断。这篇文章就记录一下我在1月21日这一天里处理的三件小事——一个权限需求、一堆对不上数的统计数据、一个改了三遍才肯提交的Merge Request。它们单独拆开看都不难,但拼在一起,才是实习这几个月以来最真实的日常。
1. 晨会汇报把话说满之前,先学会说"卡在哪"
早会照常是15分钟,但我今天没有像以前那样只说"正在推进""差不多了"。带我的leader在昨天结束时给了我一个非常具体的建议:汇报工作别总说结果,多说阻塞点,哪怕这个阻塞点在别人听来很幼稚。今天的早会我就试了一把。
1.1 昨天的"差不多"其实差很多
昨天我接手的是一个后台管理系统的角色权限调整需求。刚拿到需求文档时,我的判断是"这不就是加个判断条件吗?"但实际操作到昨天临下班,我发现问题远没有这么简单。文档里只写了"普通运营人员不能编辑价格字段",却没说清楚:
- 这个"编辑"是指前端按钮隐藏,还是后端接口也要拦截?
- 普通运营人员能看价格,还是连价格数字都要脱敏?
- 如果运营提交了一个带上价格的修改请求,后端应该直接报错还是静默忽略?
- 超级管理员和普通管理员在同一个页面上操作,校验规则是否要区分?
我昨天下午吭哧吭哧把前端按钮的判断写完了,但和后端同学一对接口才发现,后端根本没有接收"当前操作者角色"这个参数。也就是说,就算我在前端把按钮藏得再好,别人直接调接口一样能改价格。这个漏洞让我把已经写好的小半段代码又推倒了一半。
今天早上我老老实实把这些疑问列进了晨会汇报里。气氛没有我想象的那么尴尬,leader直接帮我把需求拆成了三层判断规则:前端控制展示、后端接口校验、数据层兜底。我就顺着这个思路去补全了缺失的内容。
1.2 把"不知道"说出口是一道坎,但确实省时间
有个很常见的情况是,实习生特别怕在开会时暴露自己没理解需求。我以前也是这样,总想着先做做看,遇到了问题再查,查不到再问。但实际的经验是:你埋头研究半小时的问题,别人可能不用两分钟就能给你答案。当然,不是让你什么都张嘴要答案,而是你得先自己定位到"我具体卡在哪个点上",把问题描述清楚再去问。
我今天晨会提阻塞点用的就是这个方法。我提前把问题列在了本地笔记里,然后对着笔记本说:"需求文档里没写清楚要控制的边界,我已经排查到是接口层面缺少角色参数,但我不知道其他地方还有没有类似逻辑。"这句话的效果很好,比"我还在做"有价值得多。
2. 权限需求里最大的坑,是文档里没写出来的"默认值"
回到工位之后,我的第一件事就是继续处理昨天那个权限需求。我在心里把它拆成了三个部分:前端按钮态、接口入参、后端拦截。但真正动手的时候,发现最耗时间的不是写代码,而是确定各种默认行为。
2.1 一个被我忽略的枚举字段
后台用户角色在系统里是一个枚举字段,有超级管理员、管理员、运营、只读访客四种。需求文档说"运营不可编辑价格",但翻了最近几期的改动记录,我发现"管理员"这个角色在不同的模块里行为不太一致——有的模块管理员完全放开,有的模块管理员也只能查看。
这种不一致很难通过看代码一眼判断。我的处理方式是先写了一个简单的查询脚本,把当前系统里所有角色相关的接口权限配置拉出来,列成了一张小表格:
| 模块 | 管理员 | 运营 | 只读访客 |
|---|---|---|---|
| 商品列表查看 | 允许 | 允许 | 允许 |
| 价格编辑 | 允许 | 需拦截 | 需拦截 |
| 上下架操作 | 允许 | 需拦截 | 需拦截 |
| 活动页配置 | 需校验 | 需校验 | 禁止 |
表格拉出来之后,问题就清楚很多了。"管理员"在价格编辑这个维度上跟"超级管理员"对齐,而"运营"在部分模块里也有编辑权限,只是少一个价格字段。需求文档里那句"普通运营不能编辑"里的"普通"两个字,实际操作层面其实要落到每个具体的按钮和接口上。
2.2 问清楚"要不要兜底"比写判断逻辑更值得
我一开始只打算在前端按钮加上权限判断,然后在后端接口做一个简单的if判断。但后来多问了一句:如果后端接口被人绕过直接调用,数据层面的校验怎么办?这个问题让我多花了大概四十分钟,给底层的数据更新服务加了一层角色过滤。这四十分钟值不值,短期看效率是低了,长期看安全性高了很多。
做个生活化的类比:前端藏按钮就像锁门,后端接口校验就像小区保安,数据层兜底才是保险柜。光锁门不够,光有保安也有疏忽的时候,但保险柜能在前面两道都失守时兜住最要紧的东西。权限系统这种地方,任何一层省掉,都有可能成为线上事故的起点。
2.3 为这个需求补上的自测清单
下午联调之前,我把自测清单写在了今天这条实习日志的对应位置,这里也分享出来:
- 使用运营账号登录:价格输入框是否隐藏?直接调用更新接口是否被拦截?
- 使用管理员账号登录:所有字段可编辑,接口调用正常。
- 数据层校验:通过构造一个不带角色参数的请求,确认是否走到了拒绝逻辑。
- 使用只读访客账号登录:页面只读,接口返回无权限。
- 修改价格后刷新列表,价格是否立即更新且无缓存残留。
每一项目测通过后,我才会把这个功能标记为"可联调"。这个习惯是我最近才养成的,以前我总想着功能跑通就行,但实习这几个月我愈发确认:功能的稳定不在于你写代码那一刻的灵光一现,而在于你愿意把各种边角情况都过一遍。
3. 统计数对不上:那一个小时里我学会的排查顺序
下午的另外一项任务是把某个商品活动页的昨日访问数据汇总给运营。这类SQL统计在我实习期间已经做过好几次了,本以为就是写个查询跑一下就完事,结果发现汇总出来的数字和运营那边手工报表对不上,差了整整一百多条。
3.1 先确认"数的是同一种东西",再怀疑SQL本身
我踩的第一个坑是太急着怀疑自己的SQL写错了。运营手工报表里的数据来源是两个访问日志表的笛卡尔拼接,而我的SQL是在另一个表中做的条件过滤。两者统计口径本身就不一样,一个数的是"用户点击行为记录数",一个数的是"进入活动页的用户会话数",当然不可能相等。
所以我做的第一件事不是改SQL,而是去跟运营确认他们要的到底是哪一种口径。这个确认花了几分钟,但直接避免了我按照错误的前提写出一份"看起来很对"的新SQL。
3.2 把大查询拆成三个小查询,用肉眼核对差异
确认口径后,我重写了汇总逻辑。但新的SQL跑出来的数字依然和运营的差几条。这次我没有直接相信SQL的执行结果,而是把查询拆分成了三个步骤:
- 第一步:查活动页当日的独立用户数;
- 第二步:查这些用户的详细点击记录;
- 第三步:按来源渠道分组,计算转化漏斗。
拆完之后,我每一步都单独跑一遍,用LIMIT看一眼返回的原始行数据,确认每一层的过滤条件没有把不该排除的记录排掉。最后发现是关联查询时的一个JOIN条件少了一个is_deleted字段的判断,导致某些已删除的商品记录被连带查了出来。
这一个小失误,放到一整个统计任务里,就足足多出了119条脏数据。
3.3 多一层"替运营多想想"的复核意识
问题修复后,我没有直接把数字发给运营同事,而是把统计结果的明细导出了一份CSV,用Excel人工抽查了三个渠道的数据。因为运营拿数据是要写汇报的,如果我在交付时只丢一个总结论,没有任何可回溯的明细,他们一旦追问细节,我还得重新跑一遍SQL,信任感也会打折扣。
虽然这个操作多花了十来分钟,但效果很明显。运营同事接收的时候主动说了一句"这回挺严谨啊"。我觉得,让协作对象觉得你靠谱,不是靠嘴上保证数据库没问题,而是靠交付物本身经得起追问。
4. 联调接口时最值钱的不是参数名,是边界值
权限需求做完之后,下午还剩下一个前端联调任务。这个任务不算复杂,只是给后台系统加一个分页列表的筛选功能。前后端接口已经在开发环境联通了,但一跑起来,我就发现了一些只有真实数据才会暴露出来的问题。
4.1 空列表用null还是[],这种事也要提前约定
接口文档里定义返回结构时,列表字段长这样:
{ "code": 0, "data": { "list": [], "page": 1, "size": 20, "total": 0 } }而我实际联调时,后端在没有任何数据的情况下返回的却是:
{ "code": 0, "data": { "list": null, "page": 1, "size": 20, "total": 0 } }前端在渲染列表时直接用了遍历list的函数,list为null时页面就卡在那里报错了。这个bug单独看很小,但恰恰是最容易被人忽略的约定。我在今天的日志里给自己记了一条:接口字段的默认值,必须在联调前跟后端确认清楚,特别是数组类型返回空数组还是null,整型字段返回0还是null,这类边界约定不会写入业务逻辑,但直接影响前端健壮性。
4.2 把异常分支当成"测试用例"来联调
以前我联调时,习惯是页面能正常显示就算通过。但这次我调整了一下思路:把异常分支也当成必经的验证步骤。我列出了几个用例,手动构造了空数据、单条数据、超过一页数量的数据、以及名称带特殊字符的数据,分别跑了一遍。
| 用例 | 预期结果 | 实际结果 | 结论 |
|---|---|---|---|
| 空数据 | 展示空状态占位 | 页面报错 | 需修复 |
| 单条数据 | 列表正常显示 | 正常 | 通过 |
| 超过一页数量 | 分页跳转正常 | 正常 | 通过 |
| 特殊字符 | 正常展示无注入 | 正常 | 通过 |
这个检查表看起来很简单,但在紧急联调时是真的能帮你稳住的。很多时候我们总说测试不充分,其实不是没有条件测试,而是没有把"哪些场景值得测"这个目录先列出来。
4.3 联调沟通里的一个实操细节
联调界面出现了问题,我习惯第一反应是直接把报错截图丢到群里,然后等对方回应。今天试了一次更好的做法:我先把报错日志里最关键的两行贴出来,再附一句话说明我的期望值和实际值的差异,最后给出一个最小复现路径。结果后端同事只回了一句"我看到了",然后很快就定位到了那一行空指针。
因为接口报错的问题是高频沟通场景,把信息组织清楚,本质上是在帮对方节省时间。对方节省了时间,你就能更快拿到修复后的版本,这个双赢值得坚持。
5. 第一版Merge Request被打回来三次,问题都在我"觉得没事"的地方
临下班前,我把写好的代码提交了Merge Request,结果被review的同事连续打回了三次。每一次打回的原因都不一样,但归类下来,全部集中在我自以为"没事"的细节上。
5.1 第一次打回:一个命名让我自己都忘了它的含义
改第一版时,我图省事,给一个临时变量起名叫做data1。写的时候我当然知道它是什么,但过了两个小时再回头看代码,我发现自己根本想不起来data1到底存的是哪一层的数据。
同事在review意见里写了一句:"如果过两天你来看这段代码,你还能说出它代表什么吗?"我看完有点脸红。命名这件事,说到底不是给机器看的,是给包括未来的自己在内的阅读者看的。从那次之后,我给任何超过十行的逻辑块里的临时变量,都坚持用带有业务含义的名字。
5.2 第二次打回:一个可以避免的超长if-else
我在处理权限判断时,写了一个将近四十行的if-else链。逻辑是这样的:先判断角色,再判断模块,再判断操作类型,最后返回布尔值。功能是对了,但代码读起来像在解开一团乱麻。
同事没有直接让我重写,而是建议我看看策略模式的相关资料。我花了半小时把这段逻辑改成了一张简单的配置映射表,把"角色+模块+操作"组合映射到允许/拒绝,代码量直接减少了一多半。
表格驱动的写法在这个场景下确实很清晰:权限规则本身是数据,不该写成散落在代码各处的判断语句。这一点,我在动手之前完全没意识到,纯属写了一段之后才被review逼出来的重构。
5.3 第三次打回:把自己绕进去的防御式编程
第三次打回是因为我给一个不可能为null的对象变量加了好几层非空判断。同事说这叫防御式编程过度,看起来是在防潜在问题,实际上会掩盖真实的bug来源:如果上游真的返回了null,你在这里静默跳过,反而让问题扩散得更远。
他说的一句话让我印象很深:"你该防的是接口的边界,不是每一行代码都加护垫。"从那之后,我在写判断之前会先明确,这个判断是在防真实的异常输入,还是在给自己找心理安慰。后者通常会让我停下来,重新审视数据流。
5.4 被review整理解的"较真"
三次返工确实让我有些沮丧,但更沮丧的是,每次打回其实都可以通过预先自查发现。我在等待合并的空当,把review意见整理了一张表,贴在了我这个实习阶段持续维护的笔记里:
| 常见问题 | 自查方法 |
|---|---|
| 命名含糊 | 写完两小时后重新读代码,能否看懂每一处命名 |
| 分支过长 | 统计最长的if-else区块,超过十行就考虑重构 |
| 无效防御 | 写每一处判空时,追问"这个null真的会出现吗" |
| 缺少边界说明 | 在接口文档或注释中写明空值、越界、超限行为 |
如果你准备走软件开发这条路,我特别建议你也做一张类似的表。它不会让你立刻写出完美的代码,但会显著减少你发给别人review的返工次数。
6. 下班后把这五条写进个人避坑清单
写这篇实习日志1.21的时候,已经是零点四十分。整理今天的经过,我给自己列了五条最想记住的经验:
第一,权限和统计这类功能,先确认口径再写代码。口径错了,写多少代码都是在错误的路上狂奔。
第二,汇报工作时把阻塞点说清楚,比单方面汇报进度更有价值。团队协作最怕的是一个人默默卡住却不吭声。
第三,SQL统计对不上数的时候,把这个大查询拆成小查询,肉眼核查每一层的中间结果,比反复调整最终汇总逻辑更高效。
第四,联调阶段把空数据、特殊字符、超长文本这些边界值当成正式用例,列成一张检查表去跑。
第五,代码要过得了自己这关再提Merge Request。过不了的判断标准很简单:两周后再看这段代码,你能不能毫无障碍地读懂它。
白天写代码的时候我一直在纠结"这个需求怎么实现",但回过头来记这份实习日志的时候才发现,真正的成长来自于"这个需求为什么这么定义""我为什么这么判断""有没有更稳的写法"。这些追问,在代码运行正常的时候往往最容易被跳过,所以我选择把它们写下来,留在深夜的日志里。