发布时间:2026/9/7 4:08:52
Electron inAppPurchase 中 ProductDiscount 结构体详解:字段语义、StoreKit 映射与源码实现 Electron inAppPurchase 中 ProductDiscount 结构体详解字段语义、StoreKit 映射与源码实现【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electronElectron 的ProductDiscount对象是 Mac App Store 应用内购买inAppPurchaseAPI 中的核心数据结构用于描述一个商品的折扣信息包括促销价格、支付方式按量付费、预付、免费试用以及订阅周期。掌握它的每个字段就能正确渲染首周半价之类的促销 UI、计算用户实际付费金额并在inAppPurchase.getProducts()返回结果中定位introductoryPrice与discounts两个折扣入口。本文基于仓库中的 ProductDiscount 结构文档、Product 结构文档、inAppPurchase API 文档 以及 C/Objective-C 源码in_app_purchase_product.h、in_app_purchase_product.mm、electron_api_in_app_purchase.cc逐字段说明该结构的语义、取值来源和边界行为。ProductDiscount 在 API 体系中的位置inAppPurchase是 Electron 提供的主进程模块仅用于 Mac App StoreMAS场景。调用inAppPurchase.getProducts(productIDs)后返回PromiseProduct[]而每个 Product 对象通过两个字段携带ProductDiscountintroductoryPriceProductDiscount可选—— 商品的首次引入价格introductory price即首单优惠价discountsProductDiscount[] —— 该商品所有在售折扣优惠的数组。从源码结构看这一映射关系在 Objective-C 层由 skProductToStruct: 完成当SKProduct.introductoryPrice非 nil 时转换并填入introductoryPrice当product.discounts非 nil 时逐个转换所有SKProductDiscount并 push 进discounts向量。字段逐一说明ProductDiscount共 7 个字段其中 6 个必选、1 个可选字段类型必选说明identifierstring是用于唯一标识一个折扣优惠的字符串typenumber是折扣类型pricenumber是折扣价以本地货币计priceLocalestring是用于格式化折扣价的语言环境paymentModestring是支付模式payAsYouGo/payUpFront/freeTrialnumberOfPeriodsnumber是折扣可用的期数整数subscriptionPeriodProductSubscriptionPeriod否折扣适用的订阅周期对象identifier与type折扣身份标识identifier是 App Store 为该折扣分配的唯一标识符可用于在折扣数组中区分不同优惠例如同一个季度订阅下可能同时挂有2 周试用和首月半价两个折扣。type表示折扣类型。从 skProductDiscountToStruct: 的实现可以看到这两个字段都是对 StoreKitSKProductDiscount对应属性的直接透传identifier通过base::SysNSStringToUTF8转成 UTF-8 字符串type则直接以(int)productDiscount.type强转为整数。也就是说 Electron 层不解析type的枚举含义原始枚举值会原样到达 JS 侧开发者若需区分类型可结合paymentMode一起判断。price与priceLocale价格数值与格式化price是折扣的本地货币数值double。转换源码中通过[productDiscount.price doubleValue]从NSDecimalNumber取出双精度数值因此 JS 侧拿到的是一个可直接参与算术运算的 number。priceLocale字段的实际填充方式值得特别注意在 in_app_purchase_product.mm 中当productDiscount.priceLocale非 nil 时Electron 会调用内部的formatPrice:withLocal:方法基于NSNumberFormatter的CurrencyStyle货币格式化把按该语言环境格式化后的价格字符串写入priceLocale。这与同文件内Product结构formattedPrice字段的处理逻辑一致——文档将其描述为用于格式化价格的语言环境而源码中该位置携带的是格式化结果字符串实际使用时应以其展示用价格文本的角色来对待而不是拿它做数值解析。paymentMode三种支付模式paymentMode是开发者最常使用的字段之一取值只有三种payAsYouGo—— 按量付费按订阅周期持续扣费payUpFront—— 预付一次性支付整个折扣期的费用freeTrial—— 免费试用。这一枚举映射在 skProductDiscountToStruct: 中通过比较SKProductDiscountPaymentModePayAsYouGo、SKProductDiscountPaymentModePayUpFront、SKProductDiscountPaymentModeFreeTrial三个 StoreKit 常量完成。一个典型的产品定价策略组合是订阅商品同时提供一个freeTrial折扣numberOfPeriods为试用次数加一个payAsYouGo折扣试用转正后的按周期扣费价JS 侧通常通过遍历discounts并过滤paymentMode来分别渲染。numberOfPeriods折扣可用期数numberOfPeriods指示该折扣可用多少期例如首 3 个月半价对应numberOfPeriods: 3。源码中以(int)productDiscount.numberOfPeriods转为整型存储。subscriptionPeriod可选的订阅周期subscriptionPeriod是唯一的可选字段类型为 ProductSubscriptionPeriod包含两个子字段numberOfUnitsnumber —— 订阅周期中的单位数量unitstring —— 时间增量单位可取day、week、month、year。从源码看只有当SKProductDiscount.subscriptionPeriod非 nil 时才会填充该字段见 in_app_purchase_product.mm且单位字符串由SKProductPeriodUnitDay/Week/Month/Year四个枚举逐一映射而来。对于非订阅类折扣如一次性购买的商品促销JS 侧应做undefined防御。C 数据结构与 JS 暴露链路ProductDiscount在 Electron 内部经历两层转换理解这条链路有助于排查字段为什么是这样。第一层是 C 结构体定义位于 in_app_purchase_product.hstruct ProductDiscount { std::string identifier; int type; double price 0.0; std::string priceLocale; std::string paymentMode; int numberOfPeriods; std::optionalProductSubscriptionPeriod subscriptionPeriod; };可以推断几个默认行为price默认初始化为0.0subscriptionPeriod使用std::optional表达可选性ProductDiscount的拷贝构造与默认构造均为default便于在std::vectorProductDiscount中按值传递。第二层是 gin 的 V8 转换器位于 electron_api_in_app_purchase.cc。Converterin_app_purchase::ProductDiscount::ToV8将上述 7 个字段逐个dict.Set写入 JS 对象其中subscriptionPeriod仅在has_value()为真时才写入——这保证了可选字段在 JS 侧表现为属性不存在而非null。外层Product的转换器electron_api_in_app_purchase.cc以同样方式处理introductoryPrice可选有值才 Set与discounts数组整体 Set。实战获取并解析商品的折扣信息以下示例整合了 In-App Purchases 教程 的完整工作流并展示了如何解析introductoryPrice与discounts。开发前需注意测试 IAP 必须修改node_modules/electron/dist/Electron.app/Contents/Info.plist中的CFBundleIdentifier替换为你在 iTunes Connect 中创建的 bundle identifier教程原文给出的示例keyCFBundleIdentifier/key stringcom.example.app/string// Main process const { inAppPurchase } require(electron) const PRODUCT_IDS [com.example.app.product1] // 尽早监听交易事件教程强调务必在调用 purchaseProduct 之前 inAppPurchase.on(transactions-updated, (event, transactions) { if (!Array.isArray(transactions)) return for (const transaction of transactions) { const payment transaction.payment switch (transaction.transactionState) { case purchasing: console.log(Purchasing ${payment.productIdentifier}...) break case purchased: { const receiptURL inAppPurchase.getReceiptURL() // 将 receiptURL 提交服务端校验校验通过后交付商品 inAppPurchase.finishTransactionByDate(transaction.transactionDate) break } case failed: console.log(Failed to purchase ${payment.productIdentifier}.) inAppPurchase.finishTransactionByDate(transaction.transactionDate) break // restored / deferred ... } } }) if (!inAppPurchase.canMakePayments()) { console.log(The user is not allowed to make in-app purchase.) } // 获取商品信息并解析 ProductDiscount 结构 inAppPurchase.getProducts(PRODUCT_IDS).then((products) { if (!Array.isArray(products) || products.length 0) { console.log(Unable to retrieve the product information.) return } for (const product of products) { console.log(${product.localizedTitle} 标准价: ${product.formattedPrice}) // 1. 引入价可选字段可能不存在 if (product.introductoryPrice) { const intro product.introductoryPrice console.log( 引入价 ${intro.priceLocale}${intro.paymentMode} 共 ${intro.numberOfPeriods} 期) if (intro.subscriptionPeriod) { console.log( 引入价周期: ${intro.subscriptionPeriod.numberOfUnits} ${intro.subscriptionPeriod.unit}) } } // 2. 全部在售折扣 for (const discount of product.discounts) { console.log( 折扣[${discount.identifier}] ${discount.priceLocale}) // 用 paymentMode 区分展示策略 // freeTrial - 免费试用 N 期 // payAsYouGo - N 期内每期优惠价 // payUpFront - 一次付清 if (discount.paymentMode freeTrial) { console.log( - 免费试用 ${discount.numberOfPeriods} 期) } } } // 购买教程示例用法 const selectedProduct products[0] inAppPurchase.purchaseProduct(selectedProduct.productIdentifier, 1) .then((isProductValid) { if (!isProductValid) { console.log(The product is not valid.) return } console.log(The payment has been added to the payment queue.) }) })关于行为的边界验证仓库测试 api-in-app-purchase-spec.ts 确认了getProducts()对无效商品 id 会解析为空数组——因此解析discounts前应先对products本身做非空判断示例中已包含。适用前提与限制平台限制inAppPurchase模块的创建逻辑被#if BUILDFLAG(IS_MAC)编译保护见 electron_api_in_app_purchase.ccinAppPurchase属性仅在 macOS 构建中注入且该模块面向 Mac App Store 分发签名/公证/沙箱要求见 code-signing 教程。Windows 与 Linux 上require(electron).inAppPurchase为undefined。数据源依赖ProductDiscount的所有字段均来自 App Store Connect 后台配置的 StoreKit 产品本地代码无法伪造或修改折扣字段缺失如subscriptionPeriod不存在是配置决定的正常现象必须做可选性判断。字段语义以当前仓库源码为准本文涉及的映射关系如paymentMode三值、priceLocale的填充方式均基于当前版本的 in_app_purchase_product.mm 实现升级 Electron 大版本后建议重新核对转换器代码。相关文档inAppPurchase API 完整方法列表Product 结构 / ProductSubscriptionPeriod 结构 / ProductDiscount 原始文档In-App Purchases 开发教程源码ProductDiscount 结构体定义 / StoreKit 转换实现 / gin V8 转换器【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

2026/9/7 4:03:52

复活的开源项目如何安全接入?一套可复用的评估与迁移指南

如果你最近在技术社区、资源分享帖或者聊天群里看到“真神复活,想要的自己来拿”这类标题,先别急着点下载。这两年,经典工具重新回到视野的事情并不少见,有些是原作者回归,有些是社区接手,有些是公司收购后…

2026/9/7 4:03:52

用大模型打造电商商品资料包自动体检助手

运营同事抱着一叠资料过来的时候,我以为这就是一次普通的上架前核对——6份文档加1张商品图,按以前的节奏,半小时怎么都看完了。结果那一次,她来回翻了三遍只找出两三处明显问题,真正让整个链接被平台打回的&#xff0…

2026/9/7 4:53:54

C++手写Delaunay三角网:Bowyer-Watson算法详解与性能优化

简介:一份基于C实现的Delaunay三角网算法工程包,面向计算几何初学者、GIS与有限元网格生成相关开发者,目标是以完整工程示例展示Delaunay三角剖分从数学定义到代码落地的全过程。Delaunay三角网的核心特性是任一三角形外接圆内不含其他点&…

2026/9/7 4:53:54

从被遗弃到可持续:同人服务器运维自动化实践指南

被遗弃同人服务器永恒之地,这句话看起来像某个玩家在退坑时留下的告别。放到技术视角下,它反映了很多小型社区服务器的共同处境:维护者独自承担备份、更新、兼容性修复和玩家支持,精力耗尽后留下一句“累了”,服务器从…

2026/9/7 4:53:54

Cursor中接入Grok 4.6的完整工程路径:配置、报错与成本管理

在实际 AI 编程工作流里,Grok 4.6 和 Cursor 是最近讨论度很高的两个关键词。很多人想在 Cursor 里用上 Grok 模型来写代码、读代码、生成测试,但往往卡在模型怎么接入、额度怎么算、报错怎么查这几步上。这篇文章不讨论任何非官方渠道的折扣、代充、共享…

2026/9/7 4:53:54

Word添加下划线全攻略:文字、空白横线、批量处理与打印排查

Word 里添加下划线,表面上看是办公软件最基础的操作:选中文字,按一下 CtrlU。但等你真的做合同、登记表、试卷或制度文件时就会发现,下划线背后至少还有三件事没解决:空白横线怎么做、多条横线怎么对齐、复制粘贴和打印…

2026/9/7 4:48:54

RAG检索增强生成:让大模型从凭记忆到查证回答

一个做企业内部知识库的团队曾经问过我一个很具体的问题:手里有几千份产品文档,也接入了市面上效果不错的大模型,但每次问技术细节,模型都回答得模棱两可。更头疼的是,回答出错的时候,没人能说清楚这个答案…

2026/9/7 0:47:43

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/7 0:14:19

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/7 0:14:17

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/7 0:03:36

基于YOLOv8和PyQt5的麦穗稻穗检测识别系统设计与实现

这次我们来看一个把目标检测算法和桌面端工具结合得很典型的项目:基于 YOLOv8 PyQt5 的麦穗稻穗检测识别系统。这个项目本身不是新概念,但它的价值在于落地形态很完整。YOLOv8 负责核心的麦穗稻穗目标检测,PyQt5 负责提供可视化的桌面交互界…

2026/9/7 0:03:36

UL 1642锂电池安全标准全解析:测试项目、认证流程与避坑指南

简介:UL 1642是锂电池安全领域的重要规范,本中文版资源适合锂电池制造商、检测机构工程师及产品认证相关人员阅读,用于理解电池在设计与制造层面的安全要求、测试方法与合规要点。资源共1个PDF文件,压缩包大小834KB,便…

2026/9/7 0:03:36

BS EN 13814-1-2019游乐设施安全标准:设计与制造核心要点解析

简介:BS EN 13814-1:2019是英国采纳欧洲标准EN 13814-1:2019的正式版本,由BSI标准出版,重点规定游乐设施和游乐设备在设计与制造环节的安全准则,与BS EN 13814-2:2019、BS EN 13814-3:2019共同取代旧版BS EN 13814:2004。该标准面…

2026/9/6 11:40:10

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/6 19:33:50

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/6 10:19:40

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…