Spree 6.1 B2B 订单文档体系:Documents 区域、装箱清单与付款指示打印设计全解析
【免费下载链接】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
导读
B2B 订单在成交前后会积累大量纸质文档——采购方的采购订单(PO)、报价单、装箱清单、发货文件,这些在今天的 Spree 中只能散落在邮件线程里。本文基于docs/plans/6.1-b2b-order-documents.md设计计划,系统讲解 Spree 6.1 如何为订单引入一个Documents 文档区域(上传文件、私有存储、按文档粒度的客户可见性、买家订单视图展示),并扩展 Dashboard 已有的客户端打印路径,新增箱级装箱清单与付款指示单两类可打印文档。读完本文,你将掌握Spree::OrderDocument的数据模型、私有附件与可见性设计、两类打印文档的数据来源(freight summary 与 payment terms),以及本计划与 PO 编号、批发运输、付款条款等上游计划的衔接方式,并理解为什么"发票"与 PDF 服务端渲染被刻意排除在 6.1 的 OSS 范围之外。
背景:B2B 订单的纸质文档困局
批发与 B2B 采购和零售完全不同:一单生意在真正发货前,双方往往已经交换了采购订单、报价、装箱说明、运输文件等大量资料。这些资料是订单的"事实档案"——买方财务部门几年后对账时依赖它们,海关与货运代理也需要它们。但当前 Spree 中订单模型只承载结构化数据,纸质文件的唯一去向是邮件线程,这带来两个问题:
- 文档与订单脱钩:文件存在收发双方的邮箱里,无法按订单检索,无法控制谁能看到什么;
- 打印输出能力有限:Dashboard 已有装箱单(packing slip)的客户端打印实现,但批发场景需要的箱级装箱清单(cartons/pallets/CBM/毛重)和付款指示单(terms + 银行转账信息 + 付款参考号)尚无输出载体。
该计划(docs/plans/6.1-b2b-order-documents.md)正是为了补齐这两块:一个订单级的文档存储区域,以及两类数据已就绪但尚无出口的生成式打印文档。它同时明确划定了边界——真正的发票(带编号的 proforma 与商业发票、PDF 生成、法定编号)不在 6.1 范围内,那属于 Enterprise 的 terms/invoicing 产品线。
核心设计决策(Key Decisions)
计划的开篇以"不得偏离"的强约束列出四条关键决策,它们是整个设计的骨架。
1.Spree::OrderDocument是"上传文件行",不是"生成流水线"
这是最重要的一条决策:文档区域是一个附件行模型,而不是服务器端文档生成管线。每行文档记录包含:
order_id:归属订单;label:文档标签(如"装箱清单扫描件");customer_visible:客户可见性,默认false(内部文书保持内部);- 私有存储附件:使用防伪造(spoofing-protected)校验,沿用 seller-submission / tax-certificate 先例;
uploaded_by:多态关联,记录上传者是员工还是客户——买家可以通过账户订单视图自行上传自己的文件。
计划中的po_document(来自 6.0-b2b-customer-po-numbers.md)保留其专属槽位——因为它有语义(强制必填检查)——但 Documents 区域会把它与通用文档行并列展示。
2. 生成式文档继续走客户端 HTML 打印
6.1 中两类生成文档(装箱清单、付款指示单)沿用已发布的 packing-slip 模式:刻意做成打印窗口而非 admin 路由,因为订单页面已经加载了所需全部数据。装箱清单从冻结的 freight summary 中取出 carton/pallet/CBM 区块;付款指示单渲染 terms 计划表 + 银行偏好 + 付款参考号。两者都不自称发票、不带发票编号、不产生存储工件。
3. 核心不带 PDF 栈
计划明确:当未来需要服务端渲染时,它将随 Enterprise 发票产品一起到来;OSS 的打印与上传永远不会引入 PDF 依赖。这让 OSS 维护成本保持可控,同时把"法定编号 + PDF"划给商业产品线。
4. 客户可见性按文档粒度,默认私有
内部文书保持内部;买家的订单视图只列出customer_visible: true的文档,并通过带过期时间的签名 URL(私有存储读取模式)访问。
Spree::OrderDocument模型设计
计划给出了模型的骨架代码:
class Spree::OrderDocument < Spree.base_class belongs_to :order, class_name: 'Spree::Order' belongs_to :uploaded_by, polymorphic: true, optional: true has_one_attached :file, service: Spree.private_storage_service_name validates :label, presence: true # customer_visible boolean, default false, null: false end这个模型的设计可以在仓库现有的附件先例中找到完整的实现依据:
- 私有存储服务:
Spree.private_storage_service_name是核心中处理机密文件的统一入口。Spree::TaxExemptionCertificate#document(tax_exemption_certificate.rb)与Spree::SellerRequirementSubmission#file(seller_requirement_submission.rb)都通过它挂载附件,注释明确说明原因:"Confidential, so the private service rather than the public one that serves product images"。OrderDocument#file沿用同一模式——买家的 PO 里带着价格、条款和内部成本代码,只能通过认证的下载动作访问。 - 防伪造内容校验:
Spree::SellerRequirementSubmission中的做法是"让字节决定文件是什么,而不是上传者发送的 header"(Spree::AttachmentContentTypeValidator),Spree::Purchase::PurchaseOrder模块(purchase_order.rb)则给出了更具体的清单:application/pdf、image/jpeg、image/png、image/heic、image/webp、Word 文档等,并限制单文件 10 MB——计划中的OrderDocument附件应沿用同一套校验思路。 - 有界上传:
TaxExemptionCertificate::MAX_DOCUMENT_SIZE = 10.megabytes的注释解释了原因:"the download action reads the whole blob into memory to serve it, so an unbounded upload would be an unbounded allocation per request"。PurchaseOrder还进一步实现了磁盘上实际尺寸校验(分块下载测量,防止上传者虚报byte_size绕过校验),这些约束都是OrderDocument的私有附件应当继承的安全基线。
API 表面:Admin 与 Store 两条链路
计划为文档区域规划了两条 API 链路,分工清晰:
Admin API:嵌套 CRUD + 直传
nested /admin/orders/:id/documentsCRUD;- 上传走signed blob 直传(direct upload),与服务端转存相比可避免大文件占满请求线程。
Store API:买家视角的文档索引与上传
- 账户订单视图上的
documents index:只暴露customer_visible的文档; create:买家上传自己的文件,永远对自己可见,并为员工标记为"买家提供"(buyer-supplied)。
这一"可见性过滤 + 买家可写"的组合正是文档区域的差异化价值:员工上传的内部核对单不会泄漏给买家,而买家补传的 PO 修订版又能立即回到员工视野。
Dashboard 与 Storefront 的呈现
Dashboard:订单页 Documents 卡片
订单页面新增 Documents 卡片,提供四类操作:
- 上传(upload)
- 可见性开关(visibility toggle)——对应
customer_visible字段 - 下载(download)——走签名 URL 私有读取
- 删除(带确认)(delete-with-confirm)
打印动作紧挨现有 packing slip 按钮排列,即订单页同时具备"装箱单 / 装箱清单 / 付款指示单"三类打印入口。
Storefront:完整的买家订单视图
Storefront 订单页新增文档列表,与 6.1 另外两个计划组合成买家的完整视图:
- 订单阶段 + 预计就绪日期(6.1-order-stages.md);
- 付款计划表(6.0-6.1-b2b-payment-terms.md);
- 发货 / freight summary;
- 文档列表。
两类生成式打印文档的数据来源
6.1 扩展的是 Dashboard 现有的客户端打印路径,因此理解先例至关重要。
先例:packing-slip 的客户端打印模式
仓库中已实现的原型是 packing-slip.ts。它的核心模式值得细读:
- 从
Fulfillment与Order数据构建行; - 生成完整 HTML(含内联 CSS)写入
printWindow.document,然后window.open新窗口并调用print(); - 刻意不是 admin 路由——注释写明:"the admin shell (sidebar, top bar) would print with an in-app page, and everything the slip needs is already loaded on the order screen";
- 不含价格——"a packing slip says what is in the box, not what it cost";
- 所有用户文本经
escapeHtml转义,防止订单数据注入打印 HTML。
6.1 的两类新打印将复用这一整套模式。
装箱清单(Carton-Level Packing List)
装箱清单在 packing slip 的基础上叠加物流区块,数据来自冻结的 freight summary:
- 单位数(units)
- 箱数(cartons)
- 托盘数(pallets)
- 毛重(gross weight)
- 体积 CBM
外加 PO 编号与订单号。这里的"冻结"语义来自批发运输计划(6.0-b2b-wholesale-shipping.md):freight summary 在估价时由 freight provider 计算,并持久化到所选DeliveryRate#metadata(jsonb),订单面读取一律来自那里,绝不从活跃商品目录重新推导——与 duty snapshot 的 doctrine 一致。
仓库中 freight_summary.rb 的实现给出了这个数据结构的完整形态:build从库存包装内容沿Unit → Carton → Pallet → CBM/Weight链条汇总,total_units、total_cartons、total_pallets(仅当所有箱级行都声明了cartons_per_pallet时非 nil)、total_volume(CBM)、total_weight(kg),以及complete?标志——当某行缺少箱数据时退回单位体积/重量并标记不完整,让数字保持可用同时如实告知"部分数据来自未测量的目录"。as_json的输出形状就是装箱清单打印区块直接消费的数据。
付款指示单(Payment-Instructions Sheet)
付款指示单渲染的是付款条款计划(6.0-6.1-b2b-payment-terms.md)中的数据:
- 订单号 + PO 引用
- 条款快照(terms snapshot):
kind(prepaid | deposit)、deposit_percentage、balance_due_label(如"Before shipping") - 现在应付金额(amount due now)
- 银行转账信息(bank transfer details)+付款参考号(payment reference)
关键点:条款快照在订单成交时冻结(与议价价格同理,公司后续条款变更不影响已成交订单),打印读取的始终是订单快照而非重新解析;付款参考号是 6.1 存储的、可 ransack 搜索的reference列(RF/ISO 11649 结构化债权人参考风格,校验位基于付款的 prefixed id),保证银行对账单的一行能精确对应一笔付款。而派生编号R1001-P1明确不作为对账标识——兄弟记录被删除时派生编号会漂移,这正是付款条款计划记录在案的教训。计划还规定:当 PO 编号与付款参考号存在时,打印必须渲染它们(PO 计划中的文档约束同样适用)。
与上游计划的衔接:本计划的三大依赖
计划在头部声明了三个依赖,理解它们才能理解文档区域的数据从何而来:
依赖一:Customer PO Numbers(6.0-b2b-customer-po-numbers.md,已实现)
po_document是订单的第一个附件,也是本计划模型的原型。它实现为Spree::Purchase::PurchaseOrderconcern(Cart 与 Order 共用),包含po_number规范化、私有po_document附件(防伪造 + 10 MB 上限)、以及通过resolved_company读取的po_number_required?。PO 编号在成交时随购买字段复制到订单,同一 blob 重新挂到订单(blob 共享,media-library 先例)。本计划文档区域列出它时,其"必填"语义保持专属槽位——通用文档行不承担必填检查。
依赖二:B2B Wholesale Shipping(6.0-b2b-wholesale-shipping.md,已实现)
freight summary 是装箱清单打印的数据源。批发运输计划还明确了一条与本计划相关的边界:货运单据(提单、托盘标牌、装箱清单)绝不放进Spree::ShippingLabel(那张表存放带成本和退款生命周期的承运商标签),而是进入Spree::OrderDocument或本计划的客户端打印。仓库中 freight 数据已落地于 freight.rb、delivery_rate.rb、delivery_rate_provider/freight.rb 等文件。
依赖三:B2B Payment Terms(6.0-6.1-b2b-payment-terms.md,草稿中)
条款快照与银行参考喂给付款指示单。该计划同时预告了PaymentMethod::BankTransfer(6.1)加入核心、每笔付款生成存储型reference、以及 storefront pay-balance 路由——付款指示单正是把这些信息落到可打印载体的出口。
迁移路径:三阶段落地
计划给出的落地顺序清晰,且明确"Nothing to migrate"——没有数据迁移需求:
- Schema + model + admin 嵌套 CRUD + dashboard 卡片:先打通存储与管理面;
- Store 表面(文档列表 + 买家上传):打通买家可见性链路;
- 两类打印(装箱清单区块、付款指示单):最后补上输出面。
由于 6.0 的 PO 文档(po_document)与 freight summary 已实现、payment terms 仍在草稿,付款指示单的实际数据依赖会随 payment-terms 计划的推进而就绪。
约束与边界(Constraints on Current Work)
计划对后续实现设定了三条硬约束,值得任何计划读者铭记:
- 订单面向的文件必须走
OrderDocument或专属语义槽位(如po_document)——绝不允许在其他模型上挂松散附件。这是架构纪律:附件必须可检索、可控制可见性,而不是散落在任意模型上。 - OSS 中禁用发票词汇——任何模型、编号或文档都不得命名为 invoice;付款文档只能叫"payment instructions"(付款指示)。这既是合规谨慎,也是产品边界(法定编号是 Enterprise terms/invoicing 的领域)。
- 打印必须渲染
po_number与付款参考号(存在时)——PO 计划确立的文档约束在此延续,保证会计人员无论手拿哪个参考号都能对上同一笔交易。
刻意不做的事与开放问题
计划的Open Questions 为空,但有一项刻意的取舍值得强调:不把生成的打印自动附加为存储文档。理由是——"a print is a view of live data; a stored artifact is an invoicing feature":打印是对实时数据的视图(条款或物流数据变了,打印跟着变),而存储工件属于发票产品线。这一区分贯穿全计划:6.1 的文档区域只处理"上传的真实文件",打印只是"实时数据的输出窗口",两者不混为一谈。
总结
6.1-b2b-order-documents.md为 Spree 的 B2B 能力补齐了最后一块订单拼图:通过Spree::OrderDocument让纸质文档获得订单归属、私有存储与细粒度可见性;通过两类客户端打印(箱级装箱清单、付款指示单)让 freight summary 与 payment terms 这些已就绪的数据获得出口。整个设计遵循四条一贯原则:附件归口单一模型、打印不产生存储工件、OSS 不引入 PDF 与发票词汇、数据读取以冻结快照为准。它同时为 Enterprise 的 terms/invoicing 产品留下清晰的挂载点——未来若需服务端 PDF 渲染与法定编号,将在商业产品线中到来,OSS 的上传与打印路径无需为此改动。
延伸阅读
- 上游依赖:docs/plans/6.0-b2b-customer-po-numbers.md、docs/plans/6.0-b2b-wholesale-shipping.md、docs/plans/6.0-6.1-b2b-payment-terms.md
- 相邻计划:docs/plans/6.1-order-stages.md(买家订单视图的阶段与预计日期)、docs/plans/6.0-document-numbers.md(编号机制,核心从不承诺法定编号)
- 打印先例:packages/dashboard/src/lib/packing-slip.ts(客户端 HTML 打印的完整实现)
- 附件先例:spree/core/app/models/spree/tax_exemption_certificate.rb(私有附件 + 10 MB 上限)、spree/core/app/models/spree/seller_requirement_submission.rb(防伪造内容类型校验)、spree/core/app/models/concerns/spree/purchase/purchase_order.rb(PO 文档的完整实现)
- 物流数据来源:spree/core/app/models/spree/freight_summary.rb(装箱清单打印区块消费的数据结构)
【免费下载链接】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),仅供参考