Spree 欧盟法律合规实现指南:Omnibus 价格历史、GDPR 数据主体请求与匿名化工作流

发布时间:2026/9/14 13:03:50
Spree 欧盟法律合规实现指南:Omnibus 价格历史、GDPR 数据主体请求与匿名化工作流 Spree 欧盟法律合规实现指南Omnibus 价格历史、GDPR 数据主体请求与匿名化工作流【免费下载链接】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 仓库中的实施计划 docs/plans/5.4-6.0-eu-legal-compliance.md讲解 Spree 如何为核心电商引擎内置欧盟三大法规——GDPR数据可携权、删除权、同意记录、Omnibus 指令30 天价格历史与最低价披露和消费者权利指令撤回期追踪——的合规基础设施。读完后你将理解Spree::PriceHistory模型与价格回调的工作原理、GDPR 数据导出/匿名化的完整服务链路以及各 Rake 任务、Store API 参数与数据库迁移的实操方式并能对照仓库源码定位每一处实现。总体设计核心提供数据模型与服务企业版提供管理与自动化该计划的定位是让 Spree 开箱即用地适配任何欧盟商户同时为spree_enterprise预留管理面板、自动化和多司法辖区工作流的扩展点。设计原则只有一句话核心core提供数据模型、服务与 API企业版在其上叠管理 UI 与自动化。计划状态与版本归属以文档头部为准Omnibus 部分已在 Spree 5.4 交付Spree::PriceHistory模型 价格变更回调 Store API 的?expandprior_priceGDPR 主体请求、同意记录与撤回期于 2026-09-02 实现随 Spree 6.0 交付彼时在 PR 评审中依赖两个关联计划6.0-platform-auth.mdCustomer/Staff 模型重命名与 6.0-channels-catalogs-b2b.mdMarket 模型承载按市场的法务配置Cookie 同意明确不在本计划范围内留给 storefrontNext.js 等自行实现。当前仓库中该计划的大部分设计已经落地为可运行代码价格历史模型位于 spree/core/app/models/spree/price_history.rb匿名化工作流位于 spree/core/app/workflows/spree/customers/anonymize.rb数据导出服务位于 spree/core/app/services/spree/customers/data_export.rb数据库迁移位于 spree/core/db/migrate/20260902000002_add_eu_legal_compliance.rb。下文将计划中的原始设计与仓库实际实现逐一对应说明。关键设计决策计划文档列出了 11 条“未经讨论不得偏离”的决策可按主题归纳为四组Omnibus价格历史价格历史是核心的一等模型而非插件——Spree::PriceHistory由价格变更回调自动创建默认开启track_price_history店铺偏好不需要 Omnibus 合规的美国/亚太商户可按店铺关闭保留期可配置默认 30 天对应 Omnibus 指令要求Rake 任务负责清理过期记录prior_price通过?expandprior_price选择性开启仅用于商品详情页PDP列表页PLP不展示——每次展示一条单行MIN查询即可无需预加载。GDPR数据生命周期客户数据导出与匿名化是核心服务配 Store API 端点不是企业版专属——任何处理欧盟客户数据的商户都需要匿名化保留业务记录订单、税务记录、财务数据保留只清洗 PII订单本身永不删除同时满足 GDPR 删除权与财税留存义务营销同意获得时间戳accepts_email_marketing布尔值旁新增email_marketing_consent_updated_at列因为法律上必须能证明同意何时作出或被撤回主体访问请求是异步的且留痕POST /store/customers/me/data_requests入队一个作业、立即返回202完成后邮件发送指向私有存储文件的签名链接有未完成请求时第二次请求直接返回该未完成请求而不是发起第二次构建——待处理记录本身就是冷却机制无需单独的限流。新增Spree::DataRequest模型而非复用Spree::Export子类因为导出管线是管理端所有、CSV-only 且店铺作用域的而客户 JSON 主体导出三者都不是两个主体请求端点全部开源、两端都有POST /admin/customers/:id/anonymize与GET /admin/customers/:id/export是核心代码受write_customers权限保护——删除权与访问权请求往往以邮件形式到达商户来自无法登录的人甚至从未注册账号的访客只做自助服务就漏掉了常态场景。企业版保留的是请求队列、Art. 12(3) 期限计时器、审计轨迹、自动化留存与按市场的法务文本本计划不做 PII 静态加密——那是基础设施层加密数据库或独立工程关注点本计划聚焦数据生命周期管理。消费者权利撤回期撤回期是独立的按市场偏好锚定在“收到货”上withdrawal_period_days默认 14 天与return_window_days并列而非复用——法定权利自收到货物起算退货窗口是商户自购时刻起算的商业政策常被设为 30 天作为善意承诺。期限从订单最晚的delivered_at推算在未有任何交付时回退到completed_at。5.4 中为店铺级偏好6.0 中随 Market 模型落到市场级不同市场可能有不同法律要求。Omnibus 指令Spree::PriceHistory 价格历史模型与表结构Spree::PriceHistory为每个基础价格变更留一条快照。仓库中的实际实现在 spree/core/app/models/spree/price_history.rbmodule Spree class PriceHistory Spree.base_class belongs_to :price, class_name: Spree::Price belongs_to :variant, - { with_deleted }, class_name: Spree::Variant validates :amount, presence: true, numericality: { greater_than_or_equal_to: 0 } validates :currency, presence: true validates :recorded_at, presence: true scope :for_variant, -(variant_id) { where(variant_id: variant_id) } scope :for_currency, -(currency) { where(currency: currency) } scope :in_period, -(from, to Time.current) { where(recorded_at: from..to) } scope :recent, -(days 30) { in_period(days.days.ago) } end end计划中定义的列结构如下注意只有基础价格即price_list_id IS NULL的行会被追踪——价格列表价属于内部分群Omnibus 指令针对的是客户实际看到的价格而它始终是基础价列类型说明price_idbigint, NOT NULL外键指向spree_pricesvariant_idbigint, NOT NULL反范式化避免 join 即可快速查询amountdecimal(10,2), NOT NULL该时点价格compare_at_amountdecimal(10,2)该时点比较价currencystring, NOT NULLISO 货币代码recorded_atdatetime, NOT NULL该价格生效时间created_atdatetime记录创建时间计划中的三条索引各有明确职责[variant_id, currency, recorded_at]是最低价查询的主路径[price_id, recorded_at]用于按价格维度的历史查询[recorded_at]供保留期清理使用。价格回调自动记录历史记录逻辑是挂在Spree::Price上的after_save回调且用saved_change_to_amount?做守卫避免无关属性更新产生重复记录。仓库实际实现在 spree/core/app/models/spree/price.rbdef should_record_price_history? price_list_id.nil? amount.present? saved_change_to_amount? store_preference(:track_price_history) end def record_price_history Spree::PriceHistory.create!( price: self, variant_id: variant_id, amount: amount, compare_at_amount: compare_at_amount, currency: currency, recorded_at: Time.current ) end与计划稿的一个实质差异开关从全局Spree::Config[:track_price_history]改成了store_preference(:track_price_history)即通过 Spree::StorePreferences 走店铺级偏好配合preference_store取 variant → product → store逐店判定是否记录。价格模型还声明了has_many :price_histories, dependent: :delete_all删除价格行时其历史随之清除。30 天最低价查询prior_pricespree/core/app/models/spree/price.rb#L218-L224# Returns the price history record with the lowest amount in the last 30 days # Used for EU Omnibus Directive compliance def prior_price price_histories.where(recorded_at: 30.days.ago..).order(:amount).first end注意实际实现返回的是整条PriceHistory记录按amount升序取第一条而非计划稿中的minimum(:amount)标量——这样上层可以直接取到money、display_amount、recorded_at等字段用于展示“近 30 天最低价于何时”。计划明确该查询只用于 PDPOmnibus 最低价是促销价细节没有人会在集合页展示单次展示一条MIN查询完全可接受。种子任务与保留期清理仓库中的任务文件 spree/core/lib/tasks/price_history.rake 提供两个任务1.spree:price_history:seed—— 迁移后一次性把存量价格快照为基线历史bundle exec rake spree:price_history:seed实现逻辑与计划一致遍历deleted_at: nil, price_list_id: nil的价格跳过amount为空或已有历史的行以price.updated_at || Time.current作为recorded_at并打印种子条数。2.spree:price_history:prune—— 按店铺保留期清理过期记录。实际实现比计划稿更精细值得展开task prune: :environment do total 0 Spree::Store.find_each do |store| retention_days store.preferred_price_history_retention_days next if retention_days.blank? variant_ids Spree::Variant.with_deleted. joins(:product).merge(Spree::Product.with_deleted). where(spree_products: { store_id: store.id }). select(:id) deleted Spree::PriceHistory. where(variant_id: variant_ids). where(recorded_at: ...retention_days.days.ago). delete_all # ... end # 兜底变体/商品被硬删除后不再归属任何店铺的历史行 # 按全店最长保留窗口清扫避免超出所有保留窗口 longest Spree::Store.all.filter_map { |s| s.preferred_price_history_retention_days }.max # ... end实现里的注释解释了三个关键细节保留期按店铺走因为“一个店铺是否记录价格历史由track_price_history决定用一个全局数字做清理会按非欧盟兄弟店铺的节奏删掉欧盟店铺需要的 Omnibus 证据”查询两侧都用with_deleted因为软删除的变体/商品仍有价格历史跳过的话这些行会无限累积最后还有一段清扫孤儿行的逻辑——变体被硬删除后其历史行不属于任何店铺的查询范围按“全店最长保留窗口”统一清扫保证不丢失任何店铺仍在依法展示的证据。Store API?expandprior_price选择性暴露计划中定义的序列化器写法Product 与 Variant 同款逻辑# Spree::Api::V3::ProductSerializer typelize prior_price: number | null attribute :prior_price, if: proc { params[:expand].include?(prior_price) } do |product, params| currency params[:currency] || Spree::Current.currency product.default_variant.price_in(currency).prior_price end调用方式GET /api/v3/store/products/:id?expandprior_price。货币解析优先取请求参数currency回退到当前作用域的Spree::Current.currency并通过 variant 的price_in走标准价格解析保证多币种场景下拿到的是该币种基础价的 30 天最低价。GDPR 基础营销同意时间戳与同意记录同意时间戳计划要求在用户表上增加email_marketing_consent_updated_at并通过模型回调保证“同意状态翻转”与“翻转时间”原子绑定。仓库中的实际迁移 spree/core/db/migrate/20260902000002_add_eu_legal_compliance.rb 在spree_customers上同时加了两个列change_table :spree_customers do |t| t.datetime :email_marketing_consent_updated_at t.string :email_marketing_consent_source t.datetime :anonymized_at, index: true end相比计划稿实际实现多了一个email_marketing_consent_source列——时间戳只回答“何时”来源列回答“因何”例如匿名化流程会写入Spree::ConsentRecord::ANONYMIZATION作为来源见下文。计划中对开发者有硬约束永远不要用update_column/update_all直接翻转accepts_email_marketing——同意时间戳与来源必须由模型回调写入裸写会产生一个“有状态、无证明”的同意记录。ConsentRecord同意的证据表同一迁移创建了spree_consent_records表模型见 spree/core/app/models/spree/consent_record.rb列说明store_id所属店铺NOT NULL索引owner多态归属客户或订单均可purpose/source/accepted目的、来源、是否接受email/ip_address/user_agent作出同意时的联系方式与设备指纹documentsjsonb——接受时展示的政策快照slug、名称、正文摘要recorded_atNOT NULL事件发生时间迁移注释点明了行式建模的动机“接受是一个会重复发生的事件——客户注册时同意条款此后每次结账又同意一次”所以做行而不是列。索引方面除[store_id, purpose, recorded_at]外还专门为按邮箱匹配建了LOWER(email)表达式索引PostgreSQL 上因为删除与导出都按邮箱大小写不敏感地匹配。计划同时划了一条红线绝不记录从未作出的同意——ConsentRecord是证据仅仅因为账号创建就写入一条记录等于伪造了买家从未做出的协议服务条款同意只在调用方显式传入terms_of_service时才记录。GDPR 数据主体请求Spree::DataRequest模型留痕 冷却 凭据计划中的核心决策 8 落到了 spree/core/app/models/spree/data_request.rb。模型注释直接引用了法条动机Art. 30 的留痕义务要求控制器能展示“哪些请求到达过、如何被答复的”而待处理状态天然充当冷却防止客户排无限数量的昂贵导出作业。关键结构class DataRequest Spree.base_class has_prefix_id :dsr has_status :pending, :processing, :completed, :failed, default: :pending ACCESS access.freeze # Art. 15 访问权 ERASURE erasure.freeze # Art. 17 删除权 KINDS [ACCESS, ERASURE].freeze DEFAULT_EXPIRY 7.days has_secure_token :download_token belongs_to :store belongs_to :customer belongs_to :requested_by, optional: true # 员工代理处理邮件请求时记录 has_one_attached :export_file, service: Spree.private_storage_service_name end几个值得注意的设计点编号has_spree_number prefix: DSR按店铺唯一[store_id, number]唯一索引7 天下载窗口DEFAULT_EXPIRY 7.days——“长到够一个人去处理邮件短到不让一份店铺所知一切的副本长期躺在那里”下载链接即凭据download_token是唯一索引的 secure token文件放在私有存储服务上requested_by可空客户自助发起时为 nil员工代邮件请求处理时记录操作人事件载荷去敏模型指定了专用事件序列化器Spree::Api::V3::DataRequestEventSerializerspree/api/app/serializers/spree/api/v3/data_request_event_serializer.rb——计划明确要求download_url不得进入事件载荷、webhook 正文或投递日志该序列化器加 webhook 脱敏名单构成纵深防御。请求流程由Spree::DataRequests::Create服务spree/core/app/services/spree/data_requests/create.rb发起、Spree::DataRequests::ProcessJob后台执行spree/core/app/jobs/spree/data_requests/process_job.rb完成步骤封装在 spree/core/app/workflows/spree/data_requests/fulfill.rb邮件投递由 spree/emails/app/subscribers/spree/data_request_email_subscriber.rb 处理。API 端点分布在 Store 端 spree/api/app/controllers/spree/api/v3/store/customer/data_requests_controller.rb 与下载控制器 spree/api/app/controllers/spree/api/v3/store/data_request_downloads_controller.rb以及 Admin 端 spree/api/app/controllers/spree/api/v3/admin/customers_controller.rb对应计划决策 9 的POST /admin/customers/:id/anonymize与GET /admin/customers/:id/export。DataExportArt. 15 / Art. 20 的全量导出计划中的DataExporter在实际代码中演化为 Spree::Customers::DataExport且覆盖面显著扩大。类注释解释了为什么是 JSON 哈希而不是管理端导出常用的 CSVArt. 20 要求“结构化的、通用的、机器可读的”格式客户数据是嵌套的订单内嵌行项目压平成电子表格会丢失法规要求保留的结构。实际实现的call返回以下区块对照计划稿的 8 项新增项以加粗标出{ account: ..., marketing_consent: ..., consent_records: ..., # 同意证据链 addresses: ..., orders: ..., draft_orders: ..., order_groups: ..., carts: ..., payment_sources: ..., connected_logins: ..., store_credits: ..., gift_cards: ..., wishlists: ..., custom_fields: ..., companies: ..., tax_identifiers: ..., exported_at: Time.current.iso8601 }与计划稿相比新增的披露面每一处都有对应理由源码注释可查草稿订单与购物车未完成的购买同样携带与正式订单相同的个人数据购物车因 Cart/Order 拆分独立成表被弃用的结账件也含收件地址同意记录布尔值背后的证据链——何时、从何处、看到的是哪个版本的政策文档关联登录 / 税务标识 / 公司成员身份VAT 或公司注册号对个人的识别力与地址相当自定义字段含私有“可见性只决定 storefront 渲染什么不决定主体有权看到自己的什么”。实现上还统一用find_each批量处理老客户的完整订单史避免全量行项目与地址快照同时驻留内存。与匿名化流程一致所有“关于这个人”的查询都同时按customer_id与原始邮箱匹配Spree::PersonalDataMatching覆盖访客结账时customer_id为空的行。匿名化工作流Spree::Customers::Anonymize计划中的DataAnonymizer服务在实际代码中是工作流 Spree::Customers::Anonymize它是唯一被认可的删除路径。工作流注释开宗明义区分了基础概念“订单不是个人数据而是附带了个人数据的财务记录”——总额、税行、行项目、支付与退款永不触碰可识别信息一律替换同时Spree::Customer在有购买记录后拒绝被destroy因为销毁已完成订单只是用一个法律义务换另一个。执行步骤perform(customer:, store:, requested_by:)的完整步骤序列capture_original_email # 先记住邮箱访客订单按地址/邮箱匹配 [hook: validate] # 宿主应用否决点纠纷/欺诈调查/法律保留可在此拒绝 remove_purchase_order_documents # 只读预扫PO 文档含抬头/姓名/地址 remove_carrier_documents # 只读预扫面单 PDF 上印着收件人 --- 事务内 --- claim_erasure # 行锁下打 anonymized_at 戳 anonymize_account # 账号主体 anonymize_address_book # 客户地址簿软删 清洗 anonymize_order_addresses # 订单/购物车地址快照 anonymize_payment_sources # 卡去姓名留后四位与有效期 anonymize_purchases # 订单/购物车/订单组清邮箱/IP/备注/metadata forget_gateway_profiles # 删除网关映射不调网关本身 anonymize_identities # 关联登录 anonymize_sessions # 刷新令牌含 IP 与 user agent anonymize_merchant_annotations # 自定义字段、标签、心愿单、公司成员、store credit 备注 remove_tax_identifiers # 账号上的税务注册号 anonymize_consent_records # 同意行保留目的/来源/时间抹掉邮箱/IP/UA anonymize_data_requests # 抹掉导出文件 作废下载 token record_consent_withdrawal # 撤回营销同意本身也是一条同意记录 remove_newsletter_subscriptions --- 事务外 --- purge_collected_attachments # 附件延迟清除提交后才执行 customer.reload publish_event customer.anonymized [hook: after_anonymize] # 商户在此处调网关 API 删除其端数据关键细节均有源码注释支撑替换值常量REDACTED_NAME Redacted“一眼可认出是墓碑而不是真有个叫 Anonymized 的人”匿名邮箱域名用 RFC 2606 保留的.invalid保证既无法投递也不会与真实邮箱冲突邮编截断到前 2 位truncated_postal_code保留“够判定税辖区、不足以定位到户”的部分城市/州/国保留街道/姓名/电话清空支付卡刻意不软删Spree::Payment#source的关联没有with_deleted作用域软删会让退款/冲正失去凭据来源只清洗持卡人姓名保留后四位与有效期使退款可追溯密码摘要置空留下旧密码意味着知道它的人能重新登录把姓名电话写回去claim_erasure的锁与幂等在客户行锁内先检查anonymized_at再盖戳对已删除的客户再次执行必须仍然可行个人数据可能事后回流重复执行是覆盖而非累积所以并发双跑是安全的且时间戳不回退——它记录的是“这个人第一次被遗忘的时刻”。行锁还兼作与并发导出请求的互斥要么导出先写入再被一并清除要么声明先落地使导出被拒回滚防护committed标志确保事务回滚时不执行附件清除、不发customer.anonymized事件、不跑after_anonymize钩子——否则回滚的删除会“清掉文件不可逆、广播成功、返回 success”而库里那个客户仍然完全可识别地址簿与订单快照的区分地址簿中未被订单指向的行才软删订单指向的地址行只清洗不软删因为税务稽查仍要能读出税辖区且只重写本人拥有或无主的行——B2B 结账可能直接指向公司共享地址重写它会覆盖所有其他成员导出文件是删除的盲区每个DataRequest的导出文件都是被删内容的完整副本所以匿名化会清除这些附件并置空download_token与expires_at让已发出的链接立即失效而不是等 7 天过期撤回即记录删除代为撤回营销同意时写一条accepted: false, source: ANONYMIZATION的ConsentRecord——同意历史是从行上读出的只翻列没有历史。模式守卫新增 PII 表必须同改删除路径计划约束中有一条机制化的保证任何携带客户可识别数据的新表必须在引入它的同一个变更里被匿名化流程覆盖否则 spree/core/spec/models/spree/customer_personal_data_coverage_spec.rb 这个模式守卫测试会失败。“加列而把删除路径留到以后正是店铺悄悄变不合规的方式。”匿名化工作流本身的测试在 spree/core/spec/workflows/spree/customers/anonymize_spec.rbAdmin 端 GDPR 端点测试在 spree/api/spec/controllers/spree/api/v3/admin/customers_gdpr_spec.rb。消费者权利指令14 天撤回期撤回期是独立的店铺/市场偏好withdrawal_period_days默认 14 天与return_window_days并列而不是复用理由是计划决策 10 所述法定权利自收到货物起算退货窗口是商户政策自购时刻起算。期限推算锚定订单最晚的delivered_at在没有任何交付时回退completed_at部分发货的订单尚未开始计时——期限从最后一个包裹算起。计划还明确了一条实现约束约束章节撤回期限只读、不落库——它由订单所属市场与其履约记录的delivered_at实时推导持久化副本在包裹交付的那一刻就会过期。对应地实际迁移文件20260902000002并未包含计划 5.4 稿中的spree_orders.withdrawal_eligible_until列印证了这一最终取舍。计划稿中对 Store API 的暴露方式# Spree::Api::V3::OrderSerializer typelize withdrawal_eligible_until: string | null attribute :withdrawal_eligible_until do |order| order.withdrawal_eligible_until.iso8601 end6.0 中该配置随 Market 模型落地计划 2.1 定义的withdrawal_period_days、price_history_display_days、data_retention_days、legal_entity_name等属性在 Market 模型到位前以店铺级偏好形式存在。企业版扩展钩子核心服务发布的事件供spree_enterprise订阅计划中的事件映射表核心事件企业版能力price.updated PriceHistory 记录Omnibus 合规仪表盘、价格变更审计日志customer.anonymizedGDPR 合规日志、DPO 通知customer.data_exported数据访问审计轨迹Art. 30营销同意变更同意管理时间线企业版功能与核心原语对应关系功能核心原语企业层GDPR 仪表盘DataExport、Anonymize搜索客户、触发导出/匿名化、查看审计日志的管理 UI自动化数据留存Anonymize 服务定时任务匿名化 N 天不活跃客户Omnibus 价格仪表盘PriceHistory 模型价格变更时间线、按产品的合规状态多司法辖区法务文本Store/Market 偏好按市场的法务文本编辑器、撤回政策模板泄露通知工作流事件系统事件跟踪、72 小时通知模板、DPA 联系人管理数据处理登记Art. 30事件系统全部 PII 访问审计日志、处理目的文档自动化撤回处理订单上的撤回期限撤回期内退货工作流自动批准数据库迁移与运维任务当前仓库的实际迁移 20260902000002_add_eu_legal_compliance.rb 创建/变更了三处PostgreSQL 8.1 迁移spree_customersemail_marketing_consent_updated_at、email_marketing_consent_source、anonymized_at索引spree_consent_records见上文同意记录一节含[store_id, purpose, recorded_at]与LOWER(email)索引spree_data_requests含[store_id, number]唯一索引、download_token唯一索引、[customer_id, kind, status]复合索引冷却查询路径与requested_by_id索引。注意计划 5.4 稿中的spree_price_histories表与spree_orders.withdrawal_eligible_until列不在该迁移内——价格历史表随 Omnibus 部分更早的 5.4 交付迁移创建见 spree/core/spec/lib/tasks/price_history_spec.rb 对应的既有基础设施撤回列则按“只读不落库”决策取消。运维侧的两个标准操作# 升级后一次性执行把存量基础价格快照为基线历史 bundle exec rake spree:price_history:seed # 建议纳入定时任务按各店铺保留期清理过期价格历史 bundle exec rake spree:price_history:prune开发约束清单仓库贡献者必读计划文档的 “Constraints on Current Work” 一节对后续开发划了硬边界摘要如下改价格必须走模型任何修改Spree::Price#amount的代码必须经模型保存而非裸 SQLupdate_columns否则after_save回调不触发、历史不落删除/清理客户必须走 Anonymize 工作流禁止直接删记录或调用旧方法旧的scramble_email_and_names只是委托给匿名化器的弃用壳6.1 移除不得新增调用方匿名化永不触碰财务数据订单总额、税额、行项目价、支付金额不可删改只清洗姓名、地址、邮箱、IP向订单 metadata 写入 PII 前必须考虑匿名化覆盖——匿名器会清空订单级非财务 PIIaccepts_email_marketing禁止裸写update_column/update_all否则同意状态失去时间证明不记录未作出的同意ConsentRecord是证据不是状态镜像导出下载链接是凭据禁止进入事件载荷、webhook 正文与投递日志。参考资料与延伸阅读法规原文GDPRRegulation (EU) 2016/679、Omnibus 指令Directive (EU) 2019/2161、消费者权利指令2011/83/EU关联计划6.0-platform-auth.mdCustomer/Staff 重命名匿名化器的 6.0 增强依赖它、6.0-channels-catalogs-b2b.mdMarket 模型承载按市场法务配置相关既有代码Spree::Exports::Customers与 CSV 客户导出DataExport 与其共享数据源但面向 Art. 20 的结构化 JSON、Spree::Price#compare_at_amountOmnibus “原价”展示——价格历史与之互补核心实现索引PriceHistory 模型、Price 回调与 prior_price、价格历史 Rake 任务、DataRequest 模型、DataExport 服务、Anonymize 工作流、合规迁移、模式守卫测试。【免费下载链接】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),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询