尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spree 媒体库(Media Library):统一管理门店图片与视频的复用、检索与删除机制

Spree 媒体库(Media Library):统一管理门店图片与视频的复用、检索与删除机制 Spree 媒体库Media Library统一管理门店图片与视频的复用、检索与删除机制【免费下载链接】spreeOpen Source eCommerce Platform for B2B, Marketplace, and Enterprise. REST API, TypeScript SDK, and production-ready Next.js storefront. Self-host it. Own your stack. No vendor lock-in. Zero platform fees.项目地址: https://gitcode.com/GitHub_Trending/sp/spreeSpree 6.x 引入了媒体库Media Library门店中所有的图片和视频不再分散在每个商品、分类与集合的图片字段里而是统一收纳在后台 Products → Media 页面下支持按文件名搜索、按类型和是否被使用筛选并且可以在决定放哪之前先上传文件。从库中挑选文件是复用reuse而非复制copy——同一张照片挂在三个商品上存储中仍然只有一个文件。本文基于.changeset/media-library.md的变更说明结合spree/admin-sdk的media资源与 Spree API 的服务端实现完整讲解媒体库的工作模型、SDK 接口与删除保护机制。读完本文你将能够理解媒体库文件-行row-挂载点placement三层数据模型调用spree/admin-sdk的media资源完成上传、检索、复用与带确认的删除读懂服务端MediaLibraryController中 422 拒绝删除、detachtrue一键解绑、distinct_by_file列表去重等关键实现。媒体库解决什么问题在媒体库之前媒体文件是挂在谁身上就属于谁商品图片归属于商品分类图片归属于分类。同一个文件被放到三个商品上就是三个独立副本无法检索这个文件在哪些地方用到了删除一个商品上的图片也不会让你知道另外两个地方还引用着它。媒体库把模型反转过来文件是一等公民挂载只是引用。变更文档中的核心陈述是Picking a file from the library reuses it rather than copying it, so the same photo on three products is one file in storage.从库中挑选文件是复用它而不是复制它因此同一张照片出现在三个商品上时存储中只有一个文件。这带来三个直接能力集中浏览与检索在 Products → Media 页面浏览全店文件按文件名搜索按类型图片/视频或是否被使用过滤先上传后安置media.create可以创建一条还没有归属任何商品的媒体行之后再通过source_media_id把它放到具体位置删除前可视化影响范围每个文件都可以先查询它在哪里被使用usage删除仍在使用的文件时商家确认后系统会一次性把它从所有挂载点移除。数据模型文件、行与挂载点要理解媒体库的 API 设计需要先理解其数据模型。Spree 的媒体记录Spree::Media本质上是一个多态挂载行它可以挂在商品product_id、变体variant_ids上也可以挂在分类、集合等拥有图片字段的对象上。生成的 Media 类型定义 展示了 admin 接口返回的完整字段interface Media { id: string; product_id: string | null; // 挂载的商品可为空 未安置 variant_ids: Arraystring; // 挂载的变体 position: number; // 在画廊中的排序 alt: string | null; // 替代文本 media_type: image | video | external_video; focal_point_x: number | null; // 焦点坐标用于裁切/缩略图 focal_point_y: number | null; external_video_url: string | null; video_provider: string | null; video_url: string | null; poster_url: string | null; original_url: string | null; // 原始尺寸及各档缩略图 URL mini_url: string | null; small_url: string | null; medium_url: string | null; large_url: string | null; xlarge_url: string | null; og_image_url: string | null; // ---- 媒体库新增的文件级字段 ---- attached: boolean; // 是否已被使用过滤条件 filename: string | null; // 文件名搜索依据 content_type: string | null; // MIME 类型 byte_size: number | null; // 字节数 embed_url: string | null; // 富文本编辑器嵌入用 URL signed_id: string | null; // 已上传 blob 的签名引用 viewable_id: string | null; // 当前挂载目标的 id download_url: string | null; metadata: Recordstring, unknown; viewable_type: string | null; // 当前挂载目标的类型 }其中attached、filename、content_type、byte_size、embed_url、signed_id正是本次变更在 admin media payload 上新增的字段。它们共同支撑了媒体库的两个核心交互按文件名/类型/使用状态过滤列表以及富文本编辑器通过embed_url在商品描述中内嵌图片这是富文本编辑器第一次支持在描述里嵌入图片。一个文件多行记录是理解复用的关键从源码结构看MediaLibraryController 注释当同一张图被放到三个商品上时数据库里是三行共享同一个存储 blob 的Spree::Media记录——库显示的是文件filesusage告诉你每个文件出现在哪里where each one appears。服务端实现MediaLibraryController媒体库的服务端入口是 Spree::Api::V3::Admin::MediaLibraryController它继承商品作用域的MediaController但丢弃了父级约束set_parent直接返回nil# 库行出生时未安置。viewable 保持为 nil # 直到有人把文件复制到某个商品上。 def build_resource current_store.media.build(media_attributes) end这一设计实现了先上传、后安置库中行没有viewable没有归属目标而把文件放上架putting a file ON a product是嵌套控制器的工作——向该商品的 media 端点 POST 时带上这行的 id 作为source_media_id。列表去重distinct_by_file复用导致同一 blob 对应多行记录因此列表接口需要按文件去重。控制器的scope方法区分两种请求def scope media current_store.media .accessible_by(current_ability, ability_action_for_request) .order(created_at: :desc) listing? ? media.distinct_by_file : media endindex列表应用distinct_by_filescope定义在 Spree::Media 模型每个文件只显示一行成员操作show/update/destroy不过滤因为被分组隐藏的行仍然是客户端可能持有 id 的真实记录若收窄查询会导致 show 或 destroy 返回 404。同时库端点是唯一会应用 admin 基类accessible_by的媒体端点父控制器的 scope 读取product而库请求没有 product角色层面的记录级权限规则仍然生效租户隔离则来自行上的store_id而非两跳之外的商品。删除保护422 usage detach媒体库的destroy是整个功能中防护最严格的路径def destroy references Spree::Media::Usage.call(media: resource).value return super if references.empty? unless detach_requested? return render_error( code: ERROR_CODES[:resource_invalid], message: Spree.t( api.errors.media_in_use, places: references.filter_map(:name).uniq.first(5).to_sentence ), status: :unprocessable_content, details: { usage: references.map { |reference| reference_payload(reference) } } ) end result Spree::Media::Destroy.call(media: resource) return head :no_content if result.success? # ... end行为链条与变更文档完全对应Every file shows where it is used before it is deleted, and deleting one that is still in use removes it from those places once the merchant confirms.文件未被使用直接走普通删除流程super文件仍在使用且未传detachtrue返回422错误信息列出前 5 个使用位置placesdetails.usage返回完整的引用列表每项包含kind、name、owner_type、owner_id、field——这正是 Dashboard 在删除前弹出的该文件正在这些地方使用确认框的数据来源传了detachtrueDashboard 在商家确认后发送调用 Spree::Media::Destroy 服务 所在目录下的销毁流程一次性把该文件从所有挂载点移除——商品画廊、分类与集合图片字段都在一次操作中处理。源码注释还明确区分了两种删除语义从库中删除 删除文件受上述保护而从某个商品画廊移除媒体是嵌套端点的destroy无此防护——因为那只是移除一个挂载点placement不是删除文件。另外注意create_from_url在库端点被显式禁用并返回 422URL 导入任务需要一个viewable作为目标而商家应该在正在填写的商品里做 URL 导入库本身只接收文件上传。usage 端点与权限映射usage动作调用Spree::Media::Usage服务返回引用列表语义上等价于读但 CanCanCan 的:read别名只覆盖 index 和 show因此控制器做了两处显式声明def read_actions super %w[usage] # 让 usage 纳入 API key scope 的读判定 end def authorize_resource!(resource resource, action action_name.to_sym) authorize!(action :usage ? :show : action, resource || Spree::Media) end从源码结构看这两处注释表明usage被映射为:show来授权——否则仅有只读权限的店员staffer会被拒绝访问使用位置而这恰恰是删除确认流程的前置读取。spree/admin-sdkmedia 资源SDK 侧的实现在 admin-client.ts 的 media 命名空间提供变更文档所列的六个操作list/get/create/update/delete/usage// packages/admin-sdk/src/admin-client.ts节选 readonly media { // GET /media —— 支持 ListParams 的分页/过滤 list: (params?: ListParams Recordstring, unknown, options?: RequestOptions) this.requestPaginatedResponseMedia(GET, /media, { ... }), // GET /media/:id get: (id: string, options?: RequestOptions) this.requestMedia(GET, /media/${id}, options), // POST /media —— 上传一个文件之后再决定它放在哪 create: (params: MediaLibraryCreateParams, options?: RequestOptions) this.requestMedia(POST, /media, { ...options, body: params }), // PATCH /media/:id update: (id: string, params: MediaUpdateParams, options?: RequestOptions) this.requestMedia(PATCH, /media/${id}, { ...options, body: params }), // DELETE /media/:id —— 仍在使用时返回 422 并附 usage // 除非传 detach一次性从所有使用位置移除 delete: (id: string, params?: { detach?: boolean }, options?: RequestOptions) this.requestvoid(DELETE, /media/${id}, { ...options, params: params?.detach ? { detach: true } : undefined, }), // GET /media/:id/usage —— 删除前先看它被用在哪里 usage: (id: string, options?: RequestOptions) this.request{ data: MediaUsageReference[] }(GET, /media/${id}/usage, options), };典型调用序列删除一个仍在使用的文件// 1. 查询使用位置展示给商家确认 const { data: usage } await adminClient.media.usage(mediaId); // 2. 确认后带 detach 删除文件与所有挂载点一并移除 await adminClient.media.delete(mediaId, { detach: true });SDK 注释同样强调了这个安全边界A file still in use is refused (422 with its usage) unlessdetachis set——不带detach的 API 客户端无法意外地从目录底下抽走一个文件。复用source_media_id 的工作原理source_media_id是连接库与具体位置的桥梁其行为在两个端点略有不同见 MediaLibraryController 顶部注释商品嵌套端点POST /products/:product_id/mediasource_media_id表示把库中这个文件放到该商品上。产品媒体创建参数中它被显式列入可写属性products_controller.rb并在 products 嵌套属性 workflow 中由place_from_library处理它通过product.store.media.find_by_param(source_media_id)解析库行——注意必须同门店查找跨门店的source_media_id不会被采纳库端点POST /mediasource_media_id表示把这个文件再复制进库里一次产生一条与原行共享 blob 的未安置行。商品创建/更新时同样支持在media数组中直接携带source_media_id例如{ media: [{ source_media_id: xxx, alt: Front view }] }状态流转测试 覆盖了这些场景包括source_media_id与其他上传参数signed_id同传时的取舍、以及外部门店文件 id 被拒绝的行为。media_controller_spec 则验证了商品级复用与跨门店拒绝。这一机制还约束了参数解析在商品媒体创建分支中携带source_media_id的请求不允许attachment、url、signed_id与其并存覆盖media_controller.rb 的permitted_params.except(...)逻辑保证复用与上传两条分支互斥、语义清晰。Dashboard 入口与使用场景变更文档描述了媒体库在 Dashboard 中的四个入口均位于 packages/dashboard 与 packages/dashboard-core 中商品画廊的 Add from library添加图片时可直接从库中挑选走上述source_media_id复用路径。库页面本身的路由见 products/media.tsx分类、集合、卖家seller图片字段的 Choose from library这些字段同样支持从库挑选。值得注意的是分类与集合图片现在也出现在媒体库中——即在这些位置上传的文件可以在别处复用反向亦然富文本编辑器的图片内嵌商品描述首次支持嵌入图片通过 payload 中的embed_url字段完成删除确认流点击删除前调用media.usage把kind/name/field渲染成该文件正在此处使用的确认列表确认后以detach: true发起删除。适用前提与边界本文描述的media资源、source_media_id与 payload 字段对应spree/admin-sdk与spree/dashboard的 minor 版本变更见 变更集需要运行包含该特性的 Spree 6.x 服务端媒体库端点不支持 URL 导入create_from_url返回 422按 URL 导入文件应在具体商品页面进行删除语义需精确区分库delete删除文件本身受 422/detach 保护商品嵌套destroy只移除挂载点源码注释还提示商品描述中嵌入的文件在detach删除后会留下一个不再可解析的 URL——描述内嵌与挂载点列表不在同一个自动清理范围内自动化脚本处理时应自行评估权限上usage被显式归入读类动作需要对应 API key scope 与 CanCanCanshow权限仅写权限的 key 无法查询使用位置。小结Spree 媒体库把文件从各实体的附属属性提升为门店级的一等资源库中按文件去重展示、source_media_id实现零拷贝复用、usage端点加 422/detach 机制把删除仍在使用的文件从一次危险操作变成查询-确认-批量解绑的可审计流程。对 API 集成方而言核心心智模型只有三句话——上传进库不带归属放上架用source_media_id删除前先查usage。相关的规划背景可参考 6.0-media-library 计划文档端到端行为则由 media_spec 与 media_controller_spec 覆盖验证。【免费下载链接】spreeOpen Source eCommerce Platform for B2B, Marketplace, and Enterprise. REST API, TypeScript SDK, and production-ready Next.js storefront. Self-host it. Own your stack. No vendor lock-in. Zero platform fees.项目地址: https://gitcode.com/GitHub_Trending/sp/spree创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表