用 Node.js 给商品图片建立 SHA-256 指纹清单
1. 为什么需要文件指纹清单商品图片在交付流程中经常被重新压缩、修改元数据或更换编码同名文件也可能被覆盖。比如图片文件叫“最终版.png”后来同名文件又被覆盖了上传记录却仍然写着旧名称。只按文件名核对资产就可能分不清上次实际用的是哪份内容。这里实现一个本地文件清单函数为文件读取字节数与 SHA-256并把调用方提供的商品信息一起记录。它不判断画面是否美观、颜色是否准确也不签发真实性凭证。文件指纹用于排查文件内容变化同一个商品重新压缩、修改元数据或换编码都可能改变它。判断两张图是否“视觉相同”是另一项任务不属于本文实现。清单里把计算字段与业务字段分开。2. 素材来源与工具边界Picset 是可产生商品图片素材的一条工具路径页面提供详情图制作。素材也可以来自摄影、设计软件或供应商。本函数只处理已保存到本地的文件不调用产品接口不假设 Picset 支持某种 API也不把工具名称当作图片已经获准上架的证明。3. 计算字段与业务字段分开文件路径、字节数与哈希由程序获得商品编码、包装版本、用途和审核状态由业务流程提供。代码可以记录一行“待确认”却不能通过文件指纹推导“已审核”。把这两个来源分开后续才知道该去修文件还是修业务登记。4. 概念关系图片文件、清单与指纹比对的概念关系可用下面的示意图理解。这是一张 AI 技术概念图不是实际哈希值、程序输出或安全认证。flowchart LR A[图片文件] -- 读取字节 -- B[字节数 SHA-256] B -- 记录 -- C[资产清单] D[商品业务字段] -- 记录 -- C C -- 后续比对 -- E[排查文件内容变化] C -- 业务判断 -- F[商品版本是否换版]5. 路径与体积约束下面限定文件在指定素材目录内允许子目录通过 realpath 再检查一次符号链接解析后的路径拒绝指向目录外的文件。读入前限制体积示例默认 20 MiB这是项目参数不是电商平台规范也不是完整的安全隔离。6. 实现 asset-manifest.cjs实现一个不修改原图的函数保存为 asset-manifest.cjs。示例用 Node.js 标准库SHA-256 接口依据官方 crypto 文档。const fs require(node:fs/promises); const path require(node:path); const { createHash } require(node:crypto); function within(root, file) { const rel path.relative(root, file); return rel ! !path.isAbsolute(rel) rel ! .. !rel.startsWith(.. path.sep); } async function assetRecord(rootDir, relative, metadata {}, maxBytes 20 * 1024 * 1024) { if (typeof relative ! string || !relative || path.isAbsolute(relative)) throw new TypeError(需相对路径); if (!Number.isSafeInteger(maxBytes) || maxBytes 0) throw new TypeError(体积上限无效); const root await fs.realpath(rootDir); const candidate path.resolve(root, relative); if (!within(root, candidate)) throw new Error(路径超出素材目录); const actual await fs.realpath(candidate); if (!within(root, actual)) throw new Error(链接指向目录外); const info await fs.stat(actual); if (!info.isFile()) throw new Error(目标不是文件); if (info.size maxBytes) throw new Error(文件过大); const bytes await fs.readFile(actual); if (bytes.length maxBytes) throw new Error(读取后文件过大); return { metadata, file: relative.split(path.sep).join(/), bytes: bytes.length, sha256: createHash(sha256).update(bytes).digest(hex) }; } module.exports { assetRecord };7. 调用方式调用时传入素材根目录、文件相对路径和业务字段例如 assetRecord(./assets, 01.png, { sku: DEMO-01, status: 待确认 })。其中编码是示例不对应真实商品。函数返回 Promise在调用脚本中使用 await或者 then 与 catch 处理结果。8. 批量清单与失败处理需要批量清单时先逐文件执行并记录失败项别因为一个文件不存在就默默跳过它。返回对象里保留的是调用时读到的文件字节。以后再对同一文件生成记录可以比较哈希并检查业务版本是否也变化。9. 哈希相同不等于业务合并文件改名但字节没变哈希可以保持相同两份业务记录的商品编码是否允许共用图片仍应由业务规则决定。不能因为哈希相同就自动合并不同商品。10. 测试边界与局限测试时不要只检查正常文件。本批交付用本机 Node 实际验证了正常文件、重复读取、同字节改名、字节变化、中文与子目录路径、目录目标、体积上限和目录逃逸等分支具体结果随包保存。测试不是对任意环境的安全保证也不证明所有图片可解码。函数没有检查扩展名、文件签名或图片宽高。即使把文本文件改名成 PNG它也能产生字节指纹不能因此将其认定为图片。若需要图像类型预检或解码应接另一道处理不把几种不同检查合成一个模糊的“素材合格”状态。11. 并发修改边界还有并发修改边界。检查路径、读取文件之间文件系统状态可能变化这个示例适合受控素材目录的日常记录不用于处理不可信用户能够同时改动的文件树。大目录也不宜无限并发读取批量版本需要另设并发与错误记录策略。12. 把清单放进交付流程将清单放进图片交付流程时先确定它帮助回答哪个问题某份发布记录对应的文件是否改变还是商品资料已经换版前者可以由指纹协助定位后者需要商品版本字段。把指纹当作辅助记录才不会让技术字段承担它无法提供的内容判断。13. 参考资料Picset产品介绍页面提示国际站升级为 Picell AINode.js v24.19.0crypto.createHash 与 Hash 文档