Spree 6.0 Store Policies:店铺与卖家政策文档的全栈实现(Admin API、Seller API、卖家入驻要求与后台管理) 📅 发布时间:2026/9/14 13:35:55 👁 浏览次数: Spree 6.0 Store Policies店铺与卖家政策文档的全栈实现Admin API、Seller API、卖家入驻要求与后台管理【免费下载链接】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本文以 Spree 6.0 实施计划 docs/plans/6.0-store-policies.md 为主体结合当前仓库源码标注为Status: Implemented讲解法律/政策类文档terms of service、privacy policy、returns policy、shipping policy 等如何从仅有 Store API 可读演进为后台全链路可管理管理员通过 Admin API 与 Dashboard 管理店铺政策卖家通过 Seller API 与 Seller Panel 管理自己的政策运营商还能把特定政策固化为卖家入驻onboarding要求。读完本文你将掌握Spree::Policy模型设计、三个 API 分支的 CRUD 契约、?expandpolicies读取模式以及卖家政策要求种类的计算式判定原理。背景Policy 模型已有的能力与缺失的后台面Spree::Policy在 6.0 之前已经是一个相当完整的模型。从 spree/core/app/models/spree/policy.rb 可以看到它出厂自带的能力前缀 IDhas_prefix_id :pol资源 ID 形如pol_abc123所有 API 查找按前缀 ID 或 slug 双通道进行FriendlyId slugfriendly_id :slug_candidates, use: %i[slugged scoped history]slug 在owner_idowner_type范围内唯一多态 ownerbelongs_to :owner, polymorphic: true——owner 可以是 store、seller 或其他组织Mobility 翻译TRANSLATABLE_FIELDS %i[name body]name与body均可按 locale 翻译body走富文本翻译净化后的富文本has_spree_rich_text :body与Spree::SanitizableRichText结合保存时对body做 HTML 净化并派生只读的body_html读取器作用域with_body/without_body按 body 是否有内容过滤、with_matching_name大小写与空白不敏感的名称匹配供种子与要求判定复用验证slug在 owner 范围内唯一、name、owner必填。而该模型通过 Store API、TypeScript SDK 与 storefront 完全可读。缺失的是所有后台操作面没有 Admin API CRUD、没有 Dashboard 页面、商家无法编辑每个店铺种子的四份政策。本文档描述的计划正是补齐这块并把政策所有权扩展到 marketplace 卖家。模型层改造删除for_store覆盖打通多租户隔离计划的第一项关键决策是删除Policy.for_store覆盖。原覆盖返回本店政策 安装实例上所有非店属政策一旦有其他 owner 类型的政策出现就会造成跨租户数据泄露。删除后通用Spree::Base.for_store会通过既有的Store has_many :policies, as: :owner自动解析为store.policies。源码印证在当前仓库中全局搜索def for_store已无任何匹配覆盖确实已移除。政策一律通过 owner 关联读取spree/core/app/models/spree/store.rb 第 291 行has_many :policies, class_name: Spree::Policy, dependent: :destroy, as: :ownerspree/core/app/models/spree/seller.rb 第 190 行同样的声明属于卖家。由于Spree::Policy不是 paranoid软删除模型卖家被软删除时会连带硬删除其政策——与店铺关联的既有行为一致。无需任何迁移spree_policies表见 spree/core/db/migrate/20250811112056_create_spree_policies.rb已包含所有必要字段另有 spree/core/db/migrate/20250902143122_fix_policies_store_association.rb 修正过 store 关联。店铺的四个种子政策从哪来种子政策并非数据库 fixture而是命名约定由 Store 模型创建时生成。看 spree/core/app/models/spree/store.rb 的create_default_policiesdef create_default_policies Spree::Events.disable do [ translate_with_store_locale_fallback(spree.terms_of_service), translate_with_store_locale_fallback(spree.privacy_policy), translate_with_store_locale_fallback(spree.returns_policy), translate_with_store_locale_fallback(spree.shipping_policy) ].each do |policy_name| # Manual exists?/create to work around Mobility bug with find_or_create_by next if policies.with_matching_name(policy_name).exists? policies.create(name: policy_name) end end end要点四个名称分别对应 i18n 键spree.terms_of_service/spree.privacy_policy/spree.returns_policy/spree.shipping_policy按店铺默认 locale 读取失败回退:en用with_matching_name大小写、空白不敏感做存在性检查避免重复种子种子政策可以被删除和重建——它们是约定而非 fixture卖家没有种子政策新卖家从零开始必须提供什么由Policy要求种类表达。Admin API v3店铺政策的完整 CRUDAdmin 分支只管理店铺自有政策作用域锁定current_store.policies。控制器spree/api/app/controllers/spree/api/v3/admin/policies_controller.rb 继承Admin::ResourceController核心声明model_classSpree::Policyserializer_classSpree.api.admin_policy_serializer见 spree/api/lib/spree/api/dependencies.rb 注册的Spree::Api::V3::Admin::PolicySerializerresource_permitted_attributes[:name, :slug, :body]——写入只接受纯body字段find_resourceID 以pol_开头则scope.find_by_prefix_id!否则scope.friendly.find按 slug与 storefront 查找约定一致scopesuper.order(:name)列表按名称排序——因为设计上没有position列scoped_resource :settings政策归属settings资源组权限上read_settings/write_settingsscope 直接适用无需改 catalog。路由在 spree/api/config/routes.rbresources :policies, concerns: [:translatable]即 Admin API 五个端点index/show/create/update/destroy且带翻译支持。Destroy 是真正的删除模型无软删除种子政策可自由删除重建。写契约body可写body_html只读计划中特意修正了6.0-admin-api.md的一个写契约错误读取时暴露body纯文本渲染与body_html净化后标记写入统一走body由Spree::SanitizableRichText在保存时净化。body_html是只读读取器、没有 writer。这与Spree::SanitizableRichText的文档化行为和所有已上线的富文本控制器如Admin::CategoriesController只允许:description而非:description_html一致。源码佐证Policy模型中的has_spree_rich_text :body与 Translation 子类中的sanitizes_rich_text :body共同完成纯文本写入、净化存储、派生 HTML 读取的闭环。序列化器管理员多看到时间戳spree/api/app/serializers/spree/api/v3/policy_serializer.rbstore 分支共用基类通过 typelize 声明name、slug为 stringbody、body_html为可空 string并渲染body经Spree::RichTextHelper.to_plain_text转纯文本body_html直接输出净化后的policy.body_htmlupdated_atiso8601——这是 store 序列化器打破不输出时间戳规则的唯一例外因为法律文档最后更新时间是顾客有权看到的信息created_at仍仅限 admin。Admin::PolicySerializer V3::PolicySerializer追加created_at/updated_at。Store API v3读取面与卖家政策嵌入Store 分支对 policy 端点的改动只有一处V3::PolicySerializer增加updated_at。其余保持只读。?expandpolicies读取卖家政策V3::SellerSerializerstore 分支在标准 expand guard 之后嵌入卖家政策见 spree/api/app/serializers/spree/api/v3/seller_serializer.rb# The sellers published policies, on request. Behind the expand guard # because a policy body is a whole legal document: a marketplaces # seller listing would otherwise carry every sellers full text on # every page. A product page reaches them as expandseller.policies. many :policies, resource: proc { Spree.api.policy_serializer }, if: proc { expand?(policies) }使用方式默认不嵌入GET /api/v3/store/sellers保持轻量不携带政策正文按需展开GET /api/v3/store/sellers/:id?expandpoliciesstorefront 卖家页按需请求无需独立端点index 与 show 载荷无分歧嵌套展开GET /api/v3/store/products/:id?expandseller.policies直接可用——product 序列化器既有的可展开seller关联会把嵌套 expand 透传下去无需额外代码。Store 分支路由见 spree/api/config/routes.rbresources :policies, only: [:index, :show]控制器 spree/api/app/controllers/spree/api/v3/store/policies_controller.rb 同时包含HttpCaching模块并支持 slug / 前缀 ID 双通道查找。Seller API v3/api/v3/seller卖家自主管理政策卖家分支让卖家在 seller panel 里管理自己的法律文档路由见 spree/api/config/routes.rbresources :policies。控制器spree/api/app/controllers/spree/api/v3/seller/policies_controller.rb 继承Seller::ResourceController只声明model_class、serializer_classSpree.api.seller_policy_serializer注册于 spree/api/lib/spree/api/dependencies.rb允许参数[:name, :slug, :body]slug 或前缀 ID 查找从店铺政策控制器复制scoped_resource :seller_profile——卖家政策附着在既有 seller-audience 资源上与 onboarding、要求提交控制器一致因为 OSS 没有 per-member 角色区分。两个值得注意的细节作用域隔离current_seller.policies通过seller_association映射spree_policies→:policies自动派生所以运营商店铺的政策和其他卖家的政策在此全部不可见——属于他人 ID 会得到 404 而非权限错误。授权走:seller_profileauthorize_resource!覆写为authorize!(action, :seller_profile)因为Spree::Policy在权限 catalog 里属于settings运营商的 key卖家永远不该持有此端点真正的问题是这个卖家能否编辑自己的记录而能碰哪条政策由上面的 scope 决定不经过 ability 层。Seller::PolicySerializer V3::PolicySerializer追加created_at/updated_at是卖家分支惯用的扩展 store 序列化器形态。Spree::SellerRequirements::Policy把政策固化为入驻要求这是计划最有意思的设计点一类从卖家数据计算得出的要求种类——无提交、无运营商审核。实现spree/core/app/models/spree/seller_requirements/policy.rbclass Policy Spree::SellerRequirement # The rows name IS the document required, so a marketplace adds this # kind once per policy it asks for. def self.allow_multiple? true end def met_by_seller?(seller) required required_policy_name return true if required.blank? seller.policies.any? do |policy| policy.name.to_s.strip.casecmp?(required) policy.with_body? end end # The document this row asks for: the operators own wording, which the # seller panel pre-fills when creating the policy so the two match by # construction. def required_policy_name display_name.to_s.strip end end设计要点一行一份文档allow_multiple?为 true 时基类已经要求每行一个name而这个名字就是所要求的文档。市场方要两份政策就添加两次该种类卖家看到两条独立跟踪的行各自独立达成——不会出现一损俱损的单行阻塞也不存在与行内 name 需要同步的第二套词汇表名称匹配casecmp?大小写不敏感比较时两侧都做 striptrim因为 preference/attribute 读取器返回的就是原始存储值正文存在性with_body?通过 Mobility fallback 读取器在 Ruby 中检查——只有写在翻译 locale 里的正文同样算数卖家政策集合很小无需 SQL 层优化注册加入 engine 的seller_requirementsconcat 列表但不进DEFAULT_KINDS——运营商按需 opt in不做任何要求就不会有界面噪音i18nseller_requirement_types.policy.{name,description}写入en.yml。与 seller panel / onboarding 的联动要求状态对象暴露required_policy_name沿用custom_fields的扩展点模式seller panel 据此提供一键创建、预填所需名称的政策——这让名称匹配通过构造成立onboarding 页面中policy种类的要求卡片列出缺失名称并通过action_url链接到政策页Admin 侧要求种类发现端点自动识别该种类它不声明 preferences运营商的表单就是行的 name 与 description无需自定义编辑器。Dashboard 与 Seller Panel后台管理页面Admin Dashboard新增Settings → Policies页面/settings/policies列表name、slug、更新日期 编辑面板name、slug、body正文编辑器使用已接线的MediaRichTextEditor媒体富文本组件删除走useConfirm并带政策名称确认无拖拽排序无 position 列admin-sdk新增policies资源与生成的AdminPolicy类型翻译管理随 docs/plans/5.5-6.0-resource-translations-api.md 落地Policy 已在可翻译注册表中本页面不自建翻译 UI新 i18n key 进入所有 locale 文件en/de/fr/zh-CN/ar/pldashboard 与 dashboard-core 按需。Seller Panel新增设置导航项与页面/settings/policies镜像 admin 页列表 编辑器。正文编辑器使用spree/dashboard-ui的纯RichTextEditor——媒体编辑器仅限 admin媒体库是 store 作用域因此 6.0 的卖家政策正文不支持嵌入图片当店铺存在生效的Policy要求时页面浮出要求但缺失的名称并提供预填名称的一键创建onboarding 的要求卡片policy种类列出缺失名称并链接到政策页seller-sdk新增policies资源组与生成的Policy类型。类型生成 / OpenAPI 与迁移路径标准流水线typelizer → Zod → integration specs → swaggerize → SDK tests。新增 admin 与 seller 政策端点的 integration specs 生成 OpenAPI 示例store sellers spec 以嵌入政策重新生成。对应测试文件位于spree/api/spec/controllers/spree/api/v3/admin/policies_controller_spec.rbspree/api/spec/controllers/spree/api/v3/seller/policies_controller_spec.rbspree/api/spec/controllers/spree/api/v3/store/policies_controller_spec.rbspree/api/spec/integration/spree/api/v3/admin/policies_spec.rb 等迁移路径无 schema 变更、无数据迁移Core删除for_store覆盖 泄露 spec给Seller加has_many :policiesAdmin API CRUD serializer specsstore serializer 加updated_atSeller API CRUD serializer specsstore seller serializer 在 show 上嵌入政策SellerRequirements::Policy种类 注册 i18n 状态暴露admin-sdk / seller-sdk 资源与生成类型OpenAPI 重新生成Dashboard Settings → Policies 页面Seller Panel Settings → Policies 页面 onboarding 卡片处理。其中步骤 1 必须早于或与步骤 3 同时落地——卖家政策行一旦存在旧的for_store覆盖就会立刻造成跨租户泄露。约束、边界与开放问题对后续开发的硬约束永不调用Policy.for_store或重新引入其覆盖只通过store.policies/seller.policies读取新政策 owner 模型声明has_many :policies, as: :owner并在自己的 API 分支建控制器Admin API policies 控制器保持 store 作用域不添加kind枚举、position列或固定政策词汇表——开放集合就是设计本身写入端点只允许纯body属性body_html只读不添加*_htmlwriter接受记录consent records属于 docs/plans/5.4-6.0-eu-legal-compliance.md 范围Spree::ConsentRecord在同意时刻快照每个政策的 slug、name、locale 与 body digest编辑政策永远不会改写用户已接受的证据继承 store seller serializermany :policies的序列化器必须重绑定或丢弃它——Admin::SellerSerializer丢弃_attributes.delete(:policies)Seller::ProfileSerializer重绑定到 seller serializer这两处问题都是因为生成类型与 OpenAPI schema 不一致才被发现的ResourceController之外的控制器没有expand管道可选关联只有在控制器自己传expand:时才可达如Seller::ProfileController#expand_list检查卖家内容的要求种类遵循Policy的形态配置进 preferences、计算在met_by_seller?、缺失信息通过状态对象结构化暴露——永远不给spree_sellers加新列。开放问题明确推迟到后续版本动态 storefront 政策链接storefront 目前硬编码四个种子 slug计划中的修复是store/info风格端点、其 serializer 嵌入政策列表id、name、slug让页脚与同意链接变成数据驱动——推迟到 store-info 端点形态确定运营商对卖家政策的可见性6.0 没有 admin 读取卖家政策的能力若出现审核需求如卖家在政策正文发布不当内容可能的形态是 admin 只读嵌套索引/admin/sellers/:id/policies等待真实审核需求解锁。相关计划文档docs/plans/6.0-admin-api.md——Policies CRUD 清单其body_html写契约行由本计划修正docs/plans/6.0-rich-text-descriptions.md——净化富文本Policy 已迁离 ActionTextdocs/plans/6.0-seller-onboarding-requirements.md——要求种类模式、评估、状态对象docs/plans/6.0-multi-vendor-marketplace.md——卖家主体与/api/v3/seller分支约定docs/plans/5.5-6.0-resource-translations-api.md——未来的政策翻译管理docs/plans/5.4-6.0-eu-legal-compliance.md——未来同意捕获工作的归属地【免费下载链接】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),仅供参考