Saleor Order 支付历史:`transactionSummaries` 投影设计解析(ADR-0010)
Saleor Order 支付历史transactionSummaries投影设计解析ADR-0010【免费下载链接】saleorSaleor Core: the high performance, composable, headless commerce API.项目地址: https://gitcode.com/gh_mirrors/sa/saleor客户可见的支付历史是 Saleor 3.23 引入的Order.transactionSummaries字段它以TransactionSummary投影projection的形式从已有的TransactionItem行数据中安全地对外暴露支付方式与金额而不是对受权限保护的Order.transactions字段做权限放宽。本文以 docs/adr/0010-order-payment-history-is-a-projection-not-an-opened-transaction.md 为骨架结合 saleor/graphql/payment/types.py、saleor/graphql/order/types.py、saleor/payment/models.py 等源码与测试深入讲解该 ADR 的决策背景、投影字段的完整设计、实现细节与安全边界帮助读者理解为什么投影是比开放权限更优的方案以及如何在 Storefront账户页、订单摘要中安全地展示资金流向。一、ADR 背景为什么需要一个客户可见的支付历史Saleor 作为 headless commerce API订单的支付数据一直通过Order.transactions返回TransactionItem!列表暴露给有权限的调用方。但 Storefront 场景下需要在客户账户页 / 订单详情页向顾客本人展示钱是如何进入和离开这笔订单的。直接在Order.transactions上放宽权限是不可行的原因有三开放整个类型会连带泄露内部信息TransactionItem携带externalUrlPSP 后台深度链接、events内部账本、员工身份、幂等键、createdBy、name、message等仅限内部/员工可见的数据。一旦字段开放这些数据全部对公众可见。id即交易 token是可被无认证调用的能力凭证TransactionItem.resolve_id直接返回root.token见 saleor/graphql/payment/types.py。而transactionInitialize/transactionProcess这两个 mutation 接受该 token 作为支付流程的凭据且可被无认证unauthenticated调用方使用。将 token 暴露给公众等于把继续处理支付的能力拱手让人。对既有类型做字段白名单allowlist不可行PermissionsField会在无权限时直接抛错见 saleor/graphql/core/fields.py其get_resolver会在设置了permissions时用one_of_permissions_required装饰 resolver若仅对部分字段放行其他字段对公众仍会抛权限错误。[TransactionItem!]!中!意味着字段值非空non-null只要列表内任一条目有一个被拒绝的字段整个订单查询就会因 GraphQL 非空冒泡null propagation而整体报错导致订单页面完全不可用。因此 ADR 的结论是新建一个投影projection类型TransactionSummary通过Order.transactionSummaries字段对外提供白名单化的只读视图。投影本质上是白名单——即使未来TransactionItem新增了字段也不会自动泄漏到TransactionSummary中。事实依据上述设计意图全部记录在 docs/adr/0010-order-payment-history-is-a-projection-not-an-opened-transaction.mdTransactionItem的完整字段集合token、name、message、pspReference、events、createdBy、externalUrl等定义于 saleor/graphql/payment/types.py。二、Schema 设计Order.transactionSummaries与TransactionSummary类型2.1 Order 上的新字段Order.transactionSummaries定义在 saleor/graphql/order/types.pytransaction_summaries NonNullList( TransactionSummary, description( Payment history of the order, with one entry per payment transaction that moved any money. Unlike transactions, it requires no permission and exposes only the payment method and the amounts, so it can be used to display payment details to the customer without exposing internal information. ADDED_IN_323 ), requiredTrue, )对应生成的 GraphQL Schemasaleor/graphql/schema.graphql Payment history of the order, with one entry per payment transaction that moved any money. Unlike transactions, it requires no permission and exposes only the payment method and the amounts, so it can be used to display payment details to the customer without exposing internal information. Added in Saleor 3.23. transactionSummaries: [TransactionSummary!]!关键点无权限要求与transactions要求MANAGE_ORDERS或HANDLE_PAYMENTS之一不同transactionSummaries不需要任何权限。对比同文件中的 transactions 字段定义 及其带one_of_permissions_required装饰器的 resolve_transactions可见两个字段的访问控制差异。[TransactionSummary!]!非空列表列表本身、列表元素均非空与transactions的[TransactionItem!]!形态一致。命名规范GraphQL 字段采用 camelCasetransactionSummariesPython 实现采用 snake_casetransaction_summaries。2.2TransactionSummary类型完整字段TransactionSummary定义于 saleor/graphql/payment/types.py是白名单投影的核心。其完整字段如下字段GraphQL 类型必填说明对应 TransactionItem 数据createdAtDateTime是支付交易创建时间created_atpaymentMethodDetailsPaymentMethodDetails否支付方式公开场景下卡号数字与有效期被剥离payment_method_type等authorizedAmountMoney是已授权总额amount_authorizedauthorizePendingAmountMoney是进行中的授权请求总额amount_authorize_pendingchargedAmountMoney是已收款总额amount_chargedchargePendingAmountMoney是进行中的收款请求总额amount_charge_pendingrefundedAmountMoney是已退款总额amount_refundedcanceledAmountMoney是已取消总额amount_canceled生成的 Schema 位于 saleor/graphql/schema.graphqltype TransactionSummary doc(category: Payments) { Date and time at which payment transaction was created. createdAt: DateTime! ...card number digits and expiration date are stripped: firstDigits, lastDigits, expMonth and expYear of CardPaymentMethodDetails are always null here. Read them through Order.transactions instead, which requires MANAGE_ORDERS or HANDLE_PAYMENTS. paymentMethodDetails: PaymentMethodDetails Total amount authorized for this payment. authorizedAmount: Money! authorizePendingAmount: Money! chargedAmount: Money! chargePendingAmount: Money! refundedAmount: Money! canceledAmount: Money! }注意TransactionSummary并没有实现Node接口不是 relay 节点因此没有id也就从根本上杜绝了 token 暴露。同时它也没有events、actions、externalUrl、pspReference、createdBy、name、message等内部字段——这正是投影即白名单的体现。2.3 金额字段的解析TransactionSummary的金额解析直接映射TransactionItem模型上的对应字段saleor/graphql/payment/types.pystaticmethod def resolve_authorized_amount(root: models.TransactionItem, _info): return root.amount_authorized staticmethod def resolve_authorize_pending_amount(root: models.TransactionItem, _info): return root.amount_authorize_pending staticmethod def resolve_charged_amount(root: models.TransactionItem, _info): return root.amount_charged staticmethod def resolve_charge_pending_amount(root: models.TransactionItem, _info): return root.amount_charge_pending staticmethod def resolve_refunded_amount(root: models.TransactionItem, _info): return root.amount_refunded staticmethod def resolve_canceled_amount(root: models.TransactionItem, _info): return root.amount_canceledTransactionItem模型saleor/payment/models.py将这些金额统称为value并提供了判断资金是否发生流动的方法def has_money_movement(self) - bool: Return True if any money was moved by this transaction. A transaction with all amounts at zero is an abandoned payment attempt. return any( ( self.authorized_value, self.authorize_pending_value, self.charged_value, self.charge_pending_value, self.refunded_value, self.refund_pending_value, self.canceled_value, self.cancel_pending_value, ) )三、查询解析零金额交易的过滤逻辑resolve_transaction_summaries是投影的入口saleor/graphql/order/types.pystaticmethod def resolve_transaction_summaries( root: SyncWebhookControlContext[models.Order], info ): return ( TransactionItemsByOrderIDLoader(info.context) .load(root.node.id) .then( lambda transactions: [ transaction for transaction in transactions if transaction.has_money_movement() ] ) )实现要点复用 DataLoader通过TransactionItemsByOrderIDLoader按订单 ID 批量加载TransactionItem与resolve_transactions使用同一 Loadersaleor/graphql/order/types.py因此同一请求中同时查询transactions与transactionSummaries不会产生重复数据库查询符合 Saleor 的 DataLoader 批量加载惯例。无权限装饰器该方法没有one_of_permissions_required装饰器与resolve_transactions形成鲜明对比——投影字段对任何能解析该订单能查询到order节点的调用方开放。零金额过滤通过has_money_movement()过滤掉所有金额皆为零的交易。这些交易是被放弃的支付尝试abandoned payment attempts对顾客没有任何信息价值。注意过滤发生在 Python 内存中加载后.then(...)过滤而非 SQL 查询层面因为底层 Loader 是通用的。边界情况已全额退款的交易仍然会返回。判断依据是has_money_movement()而非当前净额是否非零——只要交易在生命周期中移动过资金例如先收款再全额退款charged_value归零但refunded_value非零它仍会出现在投影中且chargedAmount显示为0、refundedAmount显示退款金额。这由测试 test_fully_refunded_transaction_is_returned 明确验证。四、卡数据剥离公开视图如何脱敏paymentMethodDetails复用共享的PaymentMethodDetails接口及其具体实现CardPaymentMethodDetails、GiftCardPaymentMethodDetails等但投影的 resolver 会在交易的副本上把卡号数字与有效期置空saleor/graphql/payment/types.pystaticmethod def resolve_payment_method_details(root: models.TransactionItem, _info): if not root.payment_method_type: return None # The shared CardPaymentMethodDetails resolvers read the card data # straight off the transaction, so strip it from a copy - this field is # public and the digits and expiration date must not leak. The copy is # never saved. public_transaction copy(root) public_transaction.cc_first_digits None public_transaction.cc_last_digits None public_transaction.cc_exp_month None public_transaction.cc_exp_year None return public_transaction这里的设计精妙之处在于复用而不重写CardPaymentMethodDetails的 resolver如 resolve_brand直接从TransactionItem对象上读取字段。为了不修改共享类型投影 resolver 用copy(root)复制一份内存副本在副本上把cc_first_digits、cc_last_digits、cc_exp_month、cc_exp_year置为None再交给共享类型解析。副本不会被保存数据库中的原始卡数据不受影响。保留品牌与方式名公众调用方仍然能看到支付方式name和品牌brand但看不到任何能识别具体卡片的信息firstDigits、lastDigits、expMonth、expYear恒为null。Staff 仍可读全量数据员工继续通过Order.transactionsTransactionItem.paymentMethodDetailsresolver 直接返回root见 saleor/graphql/payment/types.py读取完整卡数据需要MANAGE_ORDERS或HANDLE_PAYMENTS权限。Gift card 场景礼品卡详情brand、lastChars、isSaleorGiftcard照常返回因为礼品卡尾号不是敏感卡数据见测试 test_gift_card_details_are_returned。五、测试验证白名单与脱敏的保障测试文件 saleor/graphql/order/tests/queries/test_order_transaction_summaries.py 完整覆盖了 ADR 的各项设计承诺是理解该特性的最佳入口测试验证点test_available_to_any_requester_that_can_resolve_the_orderL65匿名客户端api_client与用户客户端user_api_client都能查询且有无MANAGE_ORDERS权限均不影响结果——证明该字段无权限门槛test_card_digits_and_expiration_date_are_strippedL98卡号为4111...1111、有效期12/2035的交易投影中firstDigits/lastDigits/expMonth/expYear均为null而name、brand保留且refresh_from_db()后原始cc_last_digits 1111未变——证明剥离发生在副本上test_gift_card_details_are_returnedL137礼品卡支付方式返回brand、lastChars、isSaleorGiftcardtest_transaction_without_any_money_movement_is_filtered_outL169只有产生资金流动的交易进入投影全零交易被过滤assert abandoned_transaction.has_money_movement() is Falsetest_fully_refunded_transaction_is_returnedL193全额退款后charged_value 0但交易仍在投影中refundedAmount正确显示退款金额test_internal_fields_are_not_exposedL220对id、token、pspReference、events { id }、actions、externalUrl查询均返回错误Cannot query field ... on type TransactionSummary——证明投影是严格的字段白名单其中test_internal_fields_are_not_exposed是 ADR投影即白名单论点的最直接证据TransactionSummary类型上根本不存在id/token/pspReference/events/actions/externalUrl这些字段因此即便未来TransactionItem新增字段只要不显式加入TransactionSummary就永远不会泄漏。测试中还提供了可直接复用的完整查询示例L12-L36query Order($id: ID!) { order(id: $id) { transactionSummaries { createdAt authorizedAmount { amount currency } authorizePendingAmount { amount currency } chargedAmount { amount currency } chargePendingAmount { amount currency } refundedAmount { amount currency } canceledAmount { amount currency } paymentMethodDetails { name ... on CardPaymentMethodDetails { brand firstDigits lastDigits expMonth expYear } } } } }六、对比与最佳实践为什么投影优于开放权限6.1 方案对比方案问题开放Order.transactions无权限泄露externalUrl、events内部账本、员工身份、幂等键、createdBy、name、messageid即 token可被无认证的transactionInitialize/transactionProcess用作支付凭据在既有TransactionItem上做字段级权限PermissionsField对无权限字段直接抛错且[TransactionItem!]!中任一非空字段被拒都会使整个订单查询因非空冒泡而失败新建TransactionSummary投影本 ADR 采用类型层面只定义白名单字段无id无 token金额字段全部非空、数据取自同一TransactionItem卡数据在副本上剥离6.2 从本 ADR 可以提炼的通用原则对外 API 优先用投影类型而非权限放宽当客户可见视图与内部完整视图差异较大时新建只读投影类型比在同一类型上做字段级权限更安全、更易维护。白名单优于黑名单投影类型只声明需要公开的字段天然免疫未来TransactionItem新增内部字段而忘记加白名单导致泄漏的风险。非空类型会放大字段级权限的故障面[Type!]!中只要一个元素的一个必填字段解析失败整个字段值为null并向上传播。如果必须在现有类型上做权限控制需评估非空冒泡对上层查询的影响。敏感数据脱敏应在 resolver 层基于副本完成Saleor 的做法copy(root)后置空敏感字段既复用了共享类型又不污染数据库原始数据测试也验证了剥离不影响存储。6.3 适用前提与限制该特性Added in Saleor 3.23ADDED_IN_323仅适用于使用新版 Transactions APITransactionItem的订单旧的 legacyPaymentAPI 不在投影范围内Order.payments已被标记 deprecated见 saleor/graphql/schema.graphql。投影仍受order节点本身的解析能力约束能解析到该订单如订单归属校验、公开结账流程等既有机制的调用方才能查询其transactionSummaries它并不绕过订单自身的可见性规则。paymentMethodDetails为可空字段无!当交易没有payment_method_type时返回null测试中test_available_to_any_requester_that_can_resolve_the_order即验证了paymentMethodDetails is None的场景L95。七、总结ADR-0010 记录了一个典型的 API 安全设计决策Saleor 没有选择在Order.transactions上放宽权限而是通过Order.transactionSummaries提供TransactionSummary投影。这个投影在类型层面就是白名单——没有id/token没有events、externalUrl等内部字段金额字段全部非空在数据层面通过has_money_movement()过滤掉无信息量的被放弃支付尝试并在交易副本上剥离卡号数字与有效期。源码中的 resolversaleor/graphql/order/types.py、类型定义saleor/graphql/payment/types.py、模型方法saleor/payment/models.py与全套测试test_order_transaction_summaries.py互相印证完整落地了 ADR 的设计意图。对于需要在 Storefront 账户页展示订单支付历史、又不想触碰权限边界的开发者transactionSummaries就是现成、安全且经过测试验证的标准答案。【免费下载链接】saleorSaleor Core: the high performance, composable, headless commerce API.项目地址: https://gitcode.com/gh_mirrors/sa/saleor创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

快图设计:基于 fabric.js 和 Vue 的开源 Canvas 图片编辑器

快图设计:基于 fabric.js 和 Vue 的开源 Canvas 图片编辑器

快图设计:基于 fabric.js 和 Vue 的开源 Canvas 图片编辑器 【免费下载链接】vue-fabric-editor 快图设计-基于fabric.js和Vue的开源图片编辑器,可自定义字体、素材、设计模板。fabric.js and Vue based image editor, can customize fonts, materials, …

2026/9/20 3:52:36 阅读更多 →
Homebrew可视化:BrewUI使用指南与Intel Mac安装排错

Homebrew可视化:BrewUI使用指南与Intel Mac安装排错

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/20 3:51:36 阅读更多 →
MLX-Audio:3 条命令在 Apple 芯片上生成本地语音

MLX-Audio:3 条命令在 Apple 芯片上生成本地语音

MLX-Audio:3 条命令在 Apple 芯片上生成本地语音 【免费下载链接】mlx-audio A text-to-speech (TTS), speech-to-text (STT) and speech-to-speech (STS) library built on Apples MLX framework, providing efficient speech analysis on Apple Silicon. 项目地…

2026/9/20 3:51:36 阅读更多 →

最新新闻

文学创作中的环境描写与心理刻画技法

文学创作中的环境描写与心理刻画技法

1. 文学创作中的环境描写技法解析雨夜独行者的场景描写堪称环境描写的经典范例。这种通过外部环境映射人物内心的创作手法,在文学创作中被称为"客观对应物"理论——即用具体可感的物象来表现抽象的情感状态。路灯在湿漉漉的街道上摇曳的描写,不…

2026/9/20 5:26:32 阅读更多 →
Homebrew可视化工具BrewUI实战:从安装到卸载残留清理全攻略

Homebrew可视化工具BrewUI实战:从安装到卸载残留清理全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/20 5:26:32 阅读更多 →
PicoClaw on Android Termux:在 ARM64 手机上安装、配置与运行轻量级终端 AI Agent 实战指南

PicoClaw on Android Termux:在 ARM64 手机上安装、配置与运行轻量级终端 AI Agent 实战指南

人工智能AI 应用AI Agent交互助手工具调用MCP ClientsAgent 记忆 【免费下载链接】picoclaw Tiny, Fast, and Deployable anywhere — automate the mundane, unleash your creativity 项目地址: https://gitcode.com/gh_mirrors/pi/picoclaw 点击查看 免费下载 本…

2026/9/20 5:26:32 阅读更多 →
BrowserSkill 中文完全指南:让 AI Agent 复用真实登录态操作浏览器而不打断你的工作

BrowserSkill 中文完全指南:让 AI Agent 复用真实登录态操作浏览器而不打断你的工作

BrowserSkill 中文完全指南:让 AI Agent 复用真实登录态操作浏览器而不打断你的工作 【免费下载链接】BrowserSkill Let AI agents use your real, logged-in browser without interrupting your work. CLI extension for browser automation across any shell-cap…

2026/9/20 5:26:32 阅读更多 →
RobotStudio 6.08 安装教程:环境配置、授权与RobotWare版本对接

RobotStudio 6.08 安装教程:环境配置、授权与RobotWare版本对接

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/20 5:26:32 阅读更多 →
STM32启动流程揭秘:从复位向量到main函数的完整链路

STM32启动流程揭秘:从复位向量到main函数的完整链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/20 5:25:31 阅读更多 →

日新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/20 0:00:46 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/20 0:00:46 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/20 0:00:46 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/20 0:00:46 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/20 0:00:46 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/20 0:00:46 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/19 23:01:36 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/19 17:50:38 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/19 23:35:34 阅读更多 →