恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Spree 6.0 B2B 前台采购:公司自助管理、公司地址簿与结账如何落地
首页
资讯中心
/
Spree 6.0 B2B 前台采购:公司自助管理、公司地址簿与结账如何落地
Spree 6.0 B2B 前台采购:公司自助管理、公司地址簿与结账如何落地
发布时间:2026/9/14 22:34:35
Spree 6.0 B2B 前台采购公司自助管理、公司地址簿与结账如何落地【免费下载链接】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 的开发计划文档 6.0-b2b-storefront-purchasing.md 展开它回答一个具体问题——当买家不是“一个人”而是“一家公司”时Spree 如何在前台storefront补齐公司自助管理界面并让结账流程真正理解“为公司采购”这一上下文。读完你可以掌握customer_id与company_id双轴模型的设计动机、公司地址簿“读继承、写归属”的实现selectable_address谓词、HasAddressBook三态默认地址标志、以及 Store API 已落地的公司自助与邀请接受端点全部附仓库内可核实的源码路径。背景底座已就绪缺的是前台与结账感知前置计划 6.0-b2b-companies-and-catalogs.md 已交付了公司树company法律实体节点与division组织节点深度上限 5、成员关系membership、邀请invitation、多态地址属主owner_type/owner_id以及 Store API 的自助管理面。本计划要补的是三块缺失见计划文档 Summary 一节前台没有 UI——成员管理、邀请、地址簿、公司订单树视图这些 API 都有但标准账户区里没有对应页面结账不认识公司——买家只能看到自己的个人地址簿邀请邮件里的接受链接是 404——Spree::CompanyMailer指向storefront_url/account/company-invitation?token…但那个页面此前并不存在。计划文档明确声明不改变购物车与订单的所有权模型——一笔 B2B 采购始终有两个买方主体下单的人 采购所属的组织这与 Shopify 的 purchasing-entity 形态、Medusa 与 BigCommerce 的 B2B 构建方向一致。因此本计划是“界面层”工作不是“schema”工作。需要说明storefront 位于独立仓库spree/storefront的6-0-dev分支本仓库承载的是后端实现、Store API 端点与这一份“两仓库共同事实记录”的计划文档。关键设计决策计划文档的约束性结论计划文档把五条决策标记为 “do not deviate without discussion”它们构成了后续所有实现的依据决策一Cart/Order 不加多态属主列地址模型能用owner_type/owner_id多态属主是因为一个地址恰好只有一个属主。而 B2B 采购有两个职责不同的买方主体人person负责认证、“我的订单”、事务邮件是企业审批与额度管控的作用对象组织organization负责税务锚点、目录与价格解析、公司订单视图。把两者坍缩进一个多态列就无法表达“Alice 替 Acme 买了这个”这一事实。Spree 的既有模型正是双轴customer_idcompany_id配合 standing 校验与完成时冻结模型无需改动。决策二OSS 是机制买家门户是 Enterprise开源前台把公司自助管理织入标准账户区成员、邀请、地址、公司订单列表样式只依赖前台自身的设计系统不允许长出任何角色感知——每个成员能看到所有公司页面授权由服务端 standing 检查强制Enterprise 版买家门户审批收件箱、支出看板、发票、角色通过 access-policy 子类收窄权限而不是靠 UI 开关。决策三结账引用公司地址簿完成时仍然拷贝前台在选中已存地址时提交shipping_address_id购物车的属主守卫写入口径从“只接受客户自有行”放宽为**“也接受本采购所属公司及其祖先节点持有的地址”**——与Company#default_billing_address走的 self-and-ancestors 链同一条链于是分部division预填时继承到的地址簿正是其买家可选的地址簿standing 已在更上游强制购物车不能命名一个其客户没有 standing 的公司完成时仍把地址拷贝到订单上地址簿行永远不会被一次销售“冻结”公司地址永远不会进入客户的地址簿反之亦然——守卫对跨越这条线的 id 的拒绝方式与拒绝其他客户的 id 完全一致。决策四多成员关系时公司上下文必须显式单一 standing 的买家静默解析sole_standing_company已交付多个成员关系的买家在结账时显式选择节点写入购物车的company_id已交付standing 校验。前台永远不在成员关系之间做猜测。清除公司选择即可个人采购目录、定价与税务锚点随之一起清除。决策五邀请接受页是本计划的组成部分不可选该未认证路由页面要处理 token 流服务的两类人群以被邀请邮箱注册的新用户账户用该邮箱创建以及登录后接受邀请的既有客户——登录账户邮箱与被邀请邮箱不符时拒绝行为与 API 一致。成功落地到公司页面。Store API 已落地的端点面以下端点在 spree/api/config/routes.rb 中可以直接核到store 作用域端点作用前台对应页面GET /store/account/companies当前买家的成员关系列表含祖先路径账户导航仅在返回成员关系时出现公司入口GET /store/companies/:id节点详情名称 祖先路径/account/companies/[id]PATCH /store/companies/:id重命名API 允许updateCompany已接线但 UI 暂不放出编辑控件见“开放问题”节点页名称只读GET/POST/DELETE /store/companies/:id/members按邮箱添加成员服务端将其转为 membership 或 invitation成员列表 按邮箱添加GET/DELETE /store/companies/:id/invitations待处理邀请与撤销节点页待处理邀请区…/companies/:id/addresses子资源地址簿 list/create/edit/delete/set-default全部已交付节点页地址簿复用前台既有 address-card 约定GET /store/companies/:id/orders该节点子树的已完成订单/account/companies/[id]/orders复用既有订单列表组件GET /store/company_invitations/:token未认证 token 查询返回公司与店铺名称邀请接受页的“谁邀请你加入什么”POST /store/company_invitations/:token/accept接受邀请注册或登录两种路径接受后跳转公司页实现集中在 companies_controller.rb 与 company_invitations_controller.rb 等控制器路由文件中对应注释直接标注了本计划文档路径。注意account/companies#index单独挂出供账户导航做“是否显示公司入口”的存在性判断——成员关系恰好一个时/account/companies直接重定向到该节点不浪费一次点击。后端增量放宽一个守卫外加被低估的假设修正计划文档诚实地记录原计划是“widen one guard”这部分成立——无迁移、无序列化器改动购物车与订单本来就会上报 company公司地址本来就有label与默认标志。但实现暴露出“按客户形态写的假设”蔓延得比预期远增量如下均可在本仓库源码中逐条核对。selectable_address属主守卫的唯一判定点Spree::Purchase::Addresses的ship_address_id/bill_address_id共用一个私有谓词spree/core/app/models/concerns/spree/purchase/addresses.rb#L192-L202def selectable_address(id) address ::Spree::Address.find_by(id: id) return nil if address.nil? return address if customer_id.present? address.customer_owned? address.owner_id customer_id company resolved_company return nil if company.nil? || address.owner_type ! Spree::Company company.self_and_ancestors.any? { |node| node.id address.owner_id } ? address : nil end规则解读第一本合格的书买家自己的地址簿customer_owned?且owner_id等于本单客户第二本合格的书本采购所属公司及其祖先节点持有的地址——即“分部可以选它已继承的总部地址”standing 不在此重复检查购物车模型层已有customer_has_standing_over_company校验spree/core/app/models/concerns/spree/purchase/company.rb#L77-L82能走到这里就意味着买家有权代表该节点行动被拒 id 的写入者行为保持不变静默置 nilself[ship_address_id] selectable_address(id).id而不是抛错。Company#address_book继承的阅读清单spree/core/app/models/spree/company.rb#L246-L248 定义了那条“本节点可发货到的地址”阅读链def address_book Spree::Address.where(owner_type: Spree::Company, owner_id: self_and_ancestors.map(:id)) end关键语义是读继承、写归属分部节点可以“读”总部的条目但不拥有它们。Store API 的授权纯粹依据scope返回内容因此分部成员不能借由这条阅读链触达父节点的条目去做写操作。与Company#addresses仅本节点自有的行形成对照——后者才是写目标。默认地址预填走同一条链default_billing_address从本节点开始逐级向上找第一个非空默认company.rb#L263-L269且defaults_are_own_addresses校验保证默认指针只能指向本节点自有的地址。Spree::HasAddressBook让“默认插槽”成为属主的声明新增 concernspree/core/app/models/concerns/spree/has_address_book.rb解决一个结构性问题客户与公司节点的默认插槽列名不同——客户叫bill_address_id/ship_address_id公司节点叫default_bill_address_id/default_ship_address_id。于是属主一次性声明自己的列名所有调用方问属主而不是按类名分支# Spree::Company 中的声明[company.rb#L41](https://link.gitcode.com/i/b86c8c8724070bf5e3188639c9426e50) has_address_book bill: :default_bill_address_id, ship: :default_ship_address_idAddresses::Create/Update服务由此对任意属主通用。计划文档特别记录了修复前的真实缺陷修复前把公司条目过一遍Update会返回成功但静默丢弃默认标志因为所有本该设置标志的分支都在找“地址背后的客户”——这个 bug 正是按客户形态写的假设渗入服务层的证据。三态默认标志是该 concern 的核心不变式assign_default_addresshas_address_book.rb#L40-L60标志值语义true提升该地址到对应插槽false仅当当前持有插槽的正是该地址时才让出插槽——别的地条目的默认不是本调用方的事nil静默不动插槽实现上让出插槽用条件UPDATE而非读后写release_default_columnshas_address_book.rb#L80-L87self.class.where(id: id, column address_id). update_all(column nil, updated_at: Time.current)WHERE子句即检查——两次请求之间落地的提升不会在释放操作中被回滚。这个竞态场景“不撤销在读取之后落地的提升”在 has_address_book_spec.rb 中有专门用例证明且对客户与公司节点两种属主都各证一遍覆盖三态规则全部分支。resolved_company显式选择优先、单 standing 静默解析、完成即冻结公司解析的完整优先级在 spree/core/app/models/concerns/spree/purchase/company.rb#L44-L52已下单且完成的订单只回答“下单时盖章的值”绝不重新解析——否则买家几个月后加入某公司会让一笔本应收税的历史订单回头拿到免税购物车显式company_id优先前台选择器写入的就是它兜底sole_standing_companySpree::Company.sole_standing_forcompany.rb#L125-L144在店铺范围内统计该客户的成员关系恰好一条才返回节点多条返回 nil——拒绝猜测因为猜测等于把一笔采购开给另一家企业。注释还解释了为何按“成员关系节点”而非按 standing 子树扩张计数对“一个父节点 三个分部的成员”而言standing 覆盖四个节点但成员关系唯一仍无歧义。配套两条模型层校验保证公司上下文不可被注入company_belongs_to_store公司按店铺作用域跨店铺节点不能给本店铺销售挂上他人的税务身份与customer_has_standing_over_company仅购物车路径已下单订单的公司来自完成时拷贝员工改单走管理端凭据。测试与验证面计划文档 “Specs” 一段列出的验证场景对应本仓库内的用例文件公司书 id 在有 standing 的购物车上被接受在无公司、跨无 standing 公司、面向后代节点、以及其他客户的行时被拒绝——后者覆盖selectable_address的全部拒绝分支三态标志规则含竞态在 spree/core/spec/models/concerns/spree/has_address_book_spec.rb 中对两种属主分别证明Store API 层的控制器/集成用例在 spree/api/spec 下如 companies/addresses_controller_spec.rb 与 store/companies_spec.rb后者含company-invitations/accept的 SDK 示例集成断言。迁移路径、约束与范围外迁移路径无需迁移——无 schema 变更、无重命名。前台工作是独立仓库里的增量页面后端增量是放宽一个接收守卫。对当前工作的约束计划文档 “Constraints on Current Work”前台公司 UI 只允许调用已交付的 Store API 自助端点不允许新增私有端点否则须先更新本计划前台任何代码不得按成员的 “role” 分支——OSS 没有角色只按 standing服务端已做设门购物车/订单代码必须继续把customer_id与company_id当作两个独立轴除已交付的sole_standing_company兜底外任何代码不得从一个推断另一个。明确不在本计划内角色、审批、支出限额、代下单、发票Enterprise 买家门户公司为目标的游客结账公司采购必须由已登录成员发起账期/净付与报价Enterprise 侧后续计划前台的(wholesale)演示路由组保持其“客户组门控目录演示”的本色与公司流组合但不共享代码路径。开放问题成员能否在前台重命名自己的公司节点Store API 允许重命名updateCompany也已接线进前台数据层但节点页把名称渲染为只读——这是刻意留白而非遗忘。OSS 没有公司角色放出编辑意味着任何成员都能重命名其他所有成员在其名下采购的组织爆炸半径大于该界面的其余部分添加成员是增量的重命名不是。商户可以从 dashboard 重命名Enterprise 侧由角色机制决定门户中谁可操作。小结与深入阅读路径本计划的价值在于把“公司作为买方主体”这件事做成了界面层增量模型双轴customer_idcompany_id、standing 校验、地址簿继承链都已在核心交付前台只是给它们装上了账户区页面、公司选择器与邀请接受页。核心源码脉络建议按以下顺序读计划文档docs/plans/6.0-b2b-storefront-purchasing.md底座docs/plans/6.0-b2b-companies-and-catalogs.md公司模型与树spree/core/app/models/spree/company.rb采购侧公司解析与 standing 校验spree/core/app/models/concerns/spree/purchase/company.rb地址守卫与三态默认标志spree/core/app/models/concerns/spree/purchase/addresses.rb、spree/core/app/models/concerns/spree/has_address_book.rbStore API 路由与控制器spree/api/config/routes.rb、spree/api/app/controllers/spree/api/v3/store配套文档B2B 商业模型 docs/use-case/b2b/b2b-commerce-model.mdx 与买家能力 docs/use-case/b2b/b2b-buyer-capabilities.mdx。适用前提说明本文所述端点与行为以当前仓库的 6.0 开发线代码为准计划文档标注该工作处于 “Implemented, in review”2026-08-27后台 PR 与前台 PR 在标注时点仍为开放状态storefront 侧页面属于独立仓库spree/storefront本文不对其内部文件路径作引用。【免费下载链接】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),仅供参考