origin,面向天猫/淘宝商家,线上域名 https://yc.alibaba.com)。
editOrigin 接口若检测到未挂接存证,直接返回 code 110: 原创未关联存证! 彻底阻断提交!因此关联存证是完成发布的唯一必经法理通道。
GoodsDetail 容器(老类组件),handleOk 是无参实现,天然丢弃了点击事件,首次提报永远不会白屏。status === 2 驳回)后,商家进入 NewOriginDetail 重新编辑补充材料,此时条件渲染激活了新重构的函数组件,有缺陷的形参默认值代码首次被执行,导入按钮当场引发全屏白屏!
| 对比维度 | ① 首次申报(顺利通过,零 Bug) | ② 小二驳回后重提(必现白屏崩溃!) |
|---|---|---|
| 业务场景 | 商家首次将刚发布的商品申报原创备案 | 小二审核驳回(status=2),告知“存证材料不充分”,要求商家更换/补充证据后重新提交 |
| 页面与容器 | containers/GoodsDetail/index.js (老申请页) |
containers/NewOriginDetail/index.js (新详情页) |
| 底层组件实现 | components/LinkLegal/index.js (老类组件) |
containers/NewOriginDetail/components/LinkLegal (新函数组件) |
| handleOk 实现 |
handleOk = () => { ... }无形参!直接从 state 取 result,完全忽略 Dialog 传出的 SyntheticEvent |
handleOk = (nextResult = result) => { ... }有形参!Dialog 传出的事件对象被当成数组写入 state 并广播给父表单! |
| 条件渲染状态 | 常驻表单编辑态 |
{data.status === 2 && <OriginForm />}仅当小二驳回(status=2)时才挂载表单,审核中或通过时为只读卡片 |
| 商户操作结果 | ✓ 成功导入存证,顺利提交申请 | 💥 点击导入瞬间全屏白屏,填写的表单全部丢失,陷入死循环! |
110 无法提交;去关联存证,前端立即白屏崩溃! 导致商家被永久卡死在“被驳回”状态,彻底错失首发上市保护期,极易引发商家的激烈投诉。
status = 2 的审核驳回状态与“驳回重新编辑”业务链路在平台早在 2023 年(乃至一期老系统)就已经存在,是核心标配流程。NewOriginDetail 里的 handleOk 也是无形参声明,同样不会读取 Dialog 传出的事件!
2829367934(原创商品 500238481 / 500239014)被小二驳回。页面渲染 status === 2 专属编辑表单,商家按照审核意见重新填写设计理念、修改保护范围。
code 110),商家必须重新挑选补充材料,点击弹窗开始勾选新的存证文件。
Dialog 抛出原生鼠标点击事件 SyntheticEvent,代码中直接写为 onOk={handleOk},导致事件对象直接替代了选中的数组,被当成凭证列表写入组件状态并广播给父表单!
renderItem)。执行 source.filter(...) 时,由于此时 source 是一个事件对象(Object),没有任何 filter 方法,直接在 React 渲染阶段抛出致命异常!
整个浏览器页面瞬间全屏变白(White Screen Crash)!
| 113 | onChange(changedValue); |
| 114 | }; |
| 115 | |
| 116 | -- const handleOk = (nextResult = result) => { |
| 116 | ++ const handleOk = (nextResult) => { |
| 117 | ++ let list = EMPTY_ARRAY; |
| 118 | ++ if (Array.isArray(nextResult)) { |
| 119 | ++ list = nextResult; |
| 120 | ++ } else if (Array.isArray(result)) { |
| 121 | ++ list = result; |
| 122 | ++ } |
| 123 | setVisible(false); |
| 124 | -- setSource(nextResult); |
| 125 | -- triggerChange(nextResult); |
| 124 | ++ setSource(list); |
| 125 | ++ triggerChange(list); |
| 126 | }; |
| ... | @@ -142,7 +148,8 @@ renderItem 消费端防御 @@ |
| 147 | recordTypes.forEach((r, index) => { |
| 148 | -- const selectedRecords = source?.filter(t => t.type === r) || []; |
| 148 | ++ const safeSource = Array.isArray(source) ? source : EMPTY_ARRAY; |
| 149 | ++ const selectedRecords = safeSource.filter(t => t.type === r); |
| 150 | if (recordType && selectedRecords.length === 0) return; |
| ... | @@ -190,7 +197,7 @@ Dialog onOk 调用点截断 @@ |
| 196 | className="link-dialog common-Dialog-style" |
| 197 | visible={visible} |
| 198 | -- onOk={handleOk} |
| 198 | ++ onOk={() => handleOk(result)} |
| 199 | onClose={handleCancel} |
| 200 | okText="导入" |
onOk={() => handleOk(result)} 属于“头痛医头”的点状补丁(Symptom Fix)。它把系统的健壮性完全押注在「唯一一个 JSX 属性绝对不出错」的假设上。一旦有第二处调用、父组件异常传参或未来组件重构,脆弱的防线会瞬间瓦解,白屏必定复发。
(nextResult = result) => ...。handleOk 本身依然毫无防备。任何地方一旦传递 null、0、false 或其他非数组对象,默认值立即失效,脏对象直接写入 state!
afterClose={() => handleCheckbox(false, item, handleOk)}handleOk 被作为高阶函数回调 cb 传入了 handleCheckbox!handleOk 内部紧接着执行了 triggerChange(nextResult),直接驱动外部 Form 的 onChange。| 可能发生的真实场景 | 方案 A:只改 onOk 这一处 | 方案 B:当前提交的完整修复 (3处) |
|---|---|---|
| 场景 1:用户点击弹窗「导入」按钮 | ✓ 解决(不再把事件传给 handleOk) | ✓ 彻底解决 |
| 场景 2:未来他人重构或组件升级 (例如 Dialog 升级、或抽取了通用弹窗函数) |
✗ 极易穿透 如果新人写了 onOk={handleOk},白屏立即原样复发。 |
✓ 免疫handleOk 入口 Array.isArray 自动拦截并清洗降级。 |
| 场景 3:调用方传入异常非数组数据 (如 afterClose 回调、接口异常返回 null 等) |
✗ 崩溃 直接写进 source,导致 renderItem 崩溃白屏。 |
✓ 免疫handleOk 强行归一化为安全数组。 |
| 场景 4:消费端 renderItem 遍历 (不管 state 因任何原因异常蜕变) |
✗ 脆弱source?.filter 遇到非数组 Object 必抛 TypeError,React 整树卸载白屏。 |
✓ 免疫safeSource = Array.isArray ? source : [],保证 100% 数组安全,绝不整页白屏。 |
safeSource 守卫将不可逆的整页崩溃降级为了局部的安全降级。