MoreRSS

site iconoldj | 老杰修改

男,80 后,杭州。阿里 8 年,目前在一个小而美的团队。「妙笔 / WonderPen」「ccReader 」作者。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

oldj | 老杰的 RSS 预览

富文本框架体验

2025-06-21 17:31:00

由于项目中要使用富文本编辑器,过去一段时间我深入研究了一下几个知名的富文本框架,在这儿写一下体验。

这不是一个全面的分析,仅代表个人观点。

Quill

我最开始尝试的是 Quill 项目,因为它看起来比较简单同时又足够强大,更重要的是它还有一个 Flutter 版本,可用于移动端应用的富文本开发。

实际体验上,Quill 确实比较容易上手,根据文档很容易就能写出一个基本可用的富文本编辑器来。当然,如果要添加更多复杂的或自定义功能,就要继续深入研究了。

Quill 的数据结构比较特别,它自定义了一个名为 Delta 的 JSON 格式用于描述文档,这种格式概念很简单,比如只有 insertdeleteretain 三种操作,但理论上足以描述任意复杂的文档格式以及修改。

在 Delta 的基础上,Quill 的文档可以被认为是一个“流”,只需一个数字就可以表示文档的任意位置,这让使用代码来查找、修改文档的操作变得非常简单。

一切看起来都很美好,不过当我继续深入,想实现一些更复杂的功能之时,逐渐发现 Quill 存在一些较为严重的不足。

第一个也是最大的不足,是 Quill 缺少“装饰器”设计。

所谓“装饰器”,是给文档中指定内容临时添加一些样式,但又不会影响文档数据的功能。

举例来说,实现关键词搜索时,我们需要把文档中所有匹配的关键词全部高亮显示(比如背景显示为黄色),但这个高亮样式只是临时性的,需要与富文本中原本就有的样式区分开来,添加这种装饰性的样式时,不需要触发文档的 onChange 事件,此时如果获取文档的内容,得到的数据中也不应该包含装饰性的样式。

缺少了装饰器功能,如果想高亮匹配的搜索关键词,就只能直接修改文档数据,在文档的原始数据中为这些关键词添加样式,随后在搜索结束时再去除这些样式。这便需要开发者自行添加和维护这些临时样式的状态,并区分哪些修改是真正的修改可以保存,哪些则只是装饰性修改不需要保存,显然,这会让代码逻辑变得复杂,且由于不是框架底层原生支持,性能上也会差很多。

第二个问题则和 Delta 格式有关。

使用了一段时间之后,我发现 Delta 这种格式用于描述纯文本或简单富文本的修改确实很方便,生成的 JSON 也很容易阅读,但如果要描述带有复杂嵌套结构的 HTML,Delta 反而会让问题复杂化。

同时,应该是实现上还存在 bug,当文档中有一些格式存在重叠、嵌套时,如果对它们再次格式化,可能会让内容变得混乱。对此我在 Quill 的 GitHub 仓库中提交了一个 issue ,不过暂时没有得到回应。

Slate

随后,我又尝试了 Slate ,一个基于 React 的富文本框架。

Slate 非常强大,且相较 Quill 自由度更高,基本上可以用它实现任何想要的功能。它自带装饰器(Decorations),因此,诸如高亮搜索关键词等功能的实现都变得很容易。对应地,它的概念也比 Quill 多了一些,学习成本稍高。

我用 Slate 基本实现了整个需求,原本已经基本准备正式使用它了,不过也许是我对它的理解还不够,遇到了一些始终没能处理好的小问题,虽然不影响主要功能,但却让编辑器在体验上总是差了那么一点。

比如,有时候选中了文档内容,鼠标点在空白处时不会取消选中,要再点一次才行。还有一些中文输入法似乎与它存在兼容问题,一些操作会让整个编辑器崩溃,我研究了很久也找不到修复或者处理方法,只能在外层容器捕获错误,提示用户刷新页面。

ProseMirror

最后,我决定转向 ProseMirror

ProseMirror 是一个老牌富文本框架,它的作者还写过知名的代码编辑器 CodeMirror。ProseMirror 非常强大,但因为学习曲线相对陡峭,因此劝退了很多人,包括之前的我也一度先尝试 Quill、Slate 等方案。

也有一些基于 ProseMirror 的富文本框架,比如 TipTap ,这也从另一个角度展示了 ProseMirror 的强大。

ProseMirror 的文档对新手不太友好,且它有一些自已的概念,比如文档的结构由 Schema 定义和描述,因此需要花一些时间才能上手,不过,上手之后会发现它的文档其实非常详细,基本可以从中找到需要的一切信息。

在使用 ProseMirror 将需求重新实现之后,我发现在使用 Slate 遇到的那些恼人的小问题要么没有了,要么有解决方案,而且在处理有较多装饰器的长文档时,ProseMirror 的性能似乎更好一些。

当然,ProseMirror 也不能说十全十美,不过,在深度使用了几个流行的富文本编辑器框架之后,个人觉得 ProseMirror 是综合来说最强大也最值得学习的。

小结

富文本编辑器是前端开发中最复杂也最难的主题之一,幸运的是多数情况下我们不需要完全从头开始开发,而是可以选择一个基础框架,基于它进行二次开发。

网上有很多富文本编辑器框架的评测,你可以根据自己的需求以及实际情况进行选择。

如果你希望尽快完成项目,且你的文档结构不会很复杂,也没有装饰器等需求,可以考虑 Quill。

如果你的需求比较复杂,且在使用 React,可以考虑 Slate。

最后,如果你的需求很复杂,且你的时间不那么紧张,推荐选择 ProseMirror。

使用函数计算运行定时任务

2025-05-02 15:42:00

上一篇博客提到我在使用 Dokploy 部署网站服务,但 Dokploy 不支持定时任务,于是只能创建普通服务,并在内部使用脚本定时执行命令。最近发现,将这些定时任务放在函数计算中执行可能是更好的选择。

我的服务都跑在阿里云上,下面介绍的也是阿里云函数计算。

创建函数

要创建一个函数,在阿里云函数界面后台,点击创建函数按钮即可,如下图所示:

在随后的界面中,选择“任务函数”类型。

然后,函数代码部分可根据需要选择类型,比如可以使用 ACR 中的 Docker 镜像。

需要注意的是,无论是上传代码还是使用 Docker 镜像,都要确保对应的代码能提供一个 HTTP 服务,因为定时任务执行的入口即是这个 HTTP 服务。

其他还有环境变量等配置,根据你的实际情况填写即可。

函数入口

使用函数计算的定时任务,需要你的代码提供一个 HTTP 服务,定时任务执行时,会以 POST 的方式请求 /invoke 路径,即类似下面这样的请求:

curl -X "POST" "http://localhost:8050/invoke" \
     -H 'Content-Type: application/json' \
     -d $'{
  "payload": "YOUR_PAYLOAD",
  "triggerName": "trigger-name",
  "triggerTime": "2025-04-27T03:12:45Z"
}'

当然,真实的请求还有很多 HTTP 头信息。

你需要在代码中实现 /invoke 接口,并在其中执行定时任务。在函数计算后台,可以设置超时时间等属性。

注意其中的 payload 字段,后面在设置定时触发器时,可以自定义传入的 palyload 信息。

设置定时触发器

添加函数之后,即可在配置界面设置触发器。

函数计算支持多种触发器,在这儿,我们选择定时触发器即可。

其中最后一个字段“触发消息”,其中填写的内容即是上面 payload 参数的值。注意这儿传递的是普通字符串,而不是 JSON,收到之后可根据需要做一个解析。

如果你在同一个函数中有多个用途不同的触发器,可以通过 payload 参数进行区分。

设置好之后,在函数详情界面可以看到类似下面的图示。

如果一切顺利,定时任务就添加成功了,稍后可以在日志页面看到执行记录。

更新函数

如果你的函数使用的是 ACR 中的 Docker 镜像,当推送了新的镜像时,函数计算的版本不会自动更新,需要你登录网站后台手动修改,或者调用函数计算的 API 进行设置。

每次手动修改是一件很麻烦的事,建议使用 API,以便和现有的发布流程结合起来。你可以先安装 aliyun-cli 命令行工具,然后执行类似下面的命令:

aliyun fc PUT /2023-03-30/functions/YOUR_FC --region cn-shanghai --header "Content-Type=application/json;" --body "{\"tracingConfig\":{},\"customContainerConfig\":{\"image\":\"registry-vpc.cn-shanghai.aliyuncs.com/XXX/YYY:1.2.3\"}}"

请注意将其中的参数值替换为你的项目中的值。

小结

有一些定时任务(比如清理老数据、备份用户数据等)比较耗费资源,将它们迁移到函数计算中可以减少主服务器的负担,是一个不错的实践。

函数计算是按量收费的,多数情况下,定时任务使用函数计算应该比专门买一台服务器划算,不过也不要大意,请做好优化,同时注意关注每日的用量。

使用 Dokploy 部署网站服务

2025-04-20 21:41:00

之前的几年我一直在使用 K3s + Rancher 的组合来管理网站服务,不过前段时间迁移到了 Dokploy,在这儿记录一下要点。

为什么迁移?

K3s + Rancher 的组合挺好,几年来一直运行稳定,不过对像我这样的非专业运维来说还是有点太复杂了,事实上几年来,我一直只在使用这个组合的一些最基础的功能。

去年看到有人介绍 Dokploy,了解了一下之后,发现它非常适合我的使用场景,同时又足够简单,于是花了一点时间做了研究,并最终决定迁移到 Dokploy。

除了 Dokploy 之外,还有 Coolify 等产品也不错,而且功能更多一些,读者朋友如果有需要也可以试一试。

云服务还是自托管?

Dokploy 提供了云服务,订阅之后可通过他们的云服务管理自己的服务器。

云服务听起来是个不错的选择,可以减少自己运维的时间成本,我也花了 $4.5 订阅了一个月体验了一番。不过 Dokploy 的云服务在海外,我的服务器在国内,两者之间通讯不畅,因此体验并不是很好。

最后,我选择了自托管服务,将 Dokploy 和网站服务安装在同一个网络中。

安装 Dokploy

Dokploy 的安装很简单,在一台干净的服务器上运行以下命令即可:

curl -sSL https://dokploy.com/install.sh | sh

为了确保 Dokploy 能顺利运行,这台服务器建议至少要 2 CPU + 2 G 内存。

如果你的服务器在国内,安装时可能耗时较长,可以添加国内的 docker 镜像,比如修改 /etc/docker/daemon.json 文件,添加以下内容:

{
  "registry-mirrors": [
    "https://docker.1ms.run"
  ]
}

安装完成之后,即可通过 http://{服务器 IP}:3000 的形式访问 Dokploy 后台。

添加服务器

Dokploy 成功安装后,马上就可以开始创建应用。不过,这时创建的应用会和 Dokploy 安装在同一台服务器上,你也可以在 Dokploy 后台添加新的服务器,并将应用添加到新服务器上。

个人建议用一台服务器专门运行 Dokploy,然后在 Remote Servers 面板中添加其他服务器。

添加服务器之后,还需要在 Actions 菜单中点击 Setup Server,并根据提示进行设置。

其中 Deployments 那个步骤可能耗时会很长,可以考虑点击 Modify Script,将脚本复制到对应的服务器上手动执行。

添加服务

添加完服务器之后,就可以添加项目,随后在项目中添加服务了。

添加服务这儿,最重要的一个设置是 Provider,即设置代码的来源。

Dokploy 支持多种常见的源,比如 Github,配置好之后只需向指定仓库和分支推送代码,Dokploy 就会自动拉取并构建代码,就像 Vercel 一样。

对小项目来说,这样的方式自然是很方便的,不过也可以用 Docker 作为 Provider,并使用第三方镜像服务。这样主要有两个好处:

  1. 镜像的构建工作在第三方执行,不会占用线上服务器资源;
  2. 第三方构建镜像时可以打上版本号 tag,后续回滚操作将会很方便。

我使用的是阿里云的容器镜像服务,填写方式类似下图:

更新服务

Dokploy 提供了丰富的 API,几乎所有操作都可以通过 API 完成。当某个服务需要更新时,可以登录网站手动修改相关值,也可以使用 API 更新。

比如,如果一个服务的 Provider 是 Docker,可以用类似下面的请求进行修改:

curl -X "POST" "https://your-dokploy/api/application.saveDockerProvider" \
     -H 'x-api-key: $YOUR_TOKEN' \
     -H 'Content-Type: application/json' \
     -d $'{
  "applicationId": "$APP_ID",
  "dockerImage": "$DOCKER_URL"
}'

有几个注意点:

  1. 授权头信息是 x-api-key: xxx...,而不是常见的 Authorization: Bearer xxx...
  2. applicationId 的值在 URL 中,在界面上暂时没有显示。

比如某个服务的地址是 https://your-dokploy/dashboard/project/aaa/services/application/bbb,地址最后的 bbb 就是 applicationId

通过 API 的方式,可以很方便地将服务的发布、回滚等操作集中到一处管理,或者与你现有的服务集成。

使用小结

使用 Dokploy 已经有一段时间了,整体而言还是很满意的,相对其他方案它很容易上手,且足够稳定,可用于生产环境。

不足是暂时还不支持定时任务,不过可以通过启动一个普通服务并在其中运行定时脚本的方式解决。

如果你有类似的需求,不妨也试一试 Dokploy。

Electron 中的 Kiosk 窗口

2024-11-28 21:38:00

最近在产品中用到了 Electron 中的 Kiosk 模式,记录一下要点。

什么是 Kiosk 模式?

Kiosk 模式是一种专门为限制用户操作而设计的应用运行模式,通常用于构建锁定的全屏应用程序,禁止用户访问系统其他功能或退出应用。在这种模式下,应用程序占据整个屏幕,并且用户无法通过常见的方式(如键盘快捷键、窗口控制按钮等)退出或切换到其他应用。

Kiosk 模式的主要用途是为用户提供一个专注且受限的操作环境,避免对系统的其他部分产生干扰。

哪些场景下需要使用 Kiosk 模式?

Kiosk 模式被广泛应用于以下场景:

  • 公共信息亭:自助服务终端,如银行 ATM、自助点餐机、自助售票机。
  • 展览展示:在博物馆、展览会、零售店中,用于展示信息或广告内容的屏幕。
  • 教育场景:限制学生只能使用特定的教学应用,避免访问其他不必要的内容。
  • 会议或演讲:锁定演示内容,避免误操作或退出。
  • 数字标牌:作为广告屏幕或公告牌,循环播放内容。

当然,我在开发的是日常效率软件,并不属于以上场景。我用到 Kiosk 模式的场景主要如下。

图几截图软件

我开发并维护着一个截图软件图几,它有三种截图模式:全屏截图、窗口截图、区域截图。

其中区域截图的交互方式是:用户点击截图按钮(或按下截图快捷键),先生成当前屏幕的截图,随后显示一个全屏无边框窗口,在窗口中显示将刚刚生成的屏幕截图,同时允许用户在窗口上进行框选等操作。

这个无边框窗口就需要使用 Kiosk 模式,以免用户无意中切换窗口。当然,等用户完成或取消截图时,需要再退出或关闭对应的 Kiosk 窗口。

WonderPen 写作软件

WonderPen 写作软件最近添加了小黑屋模式,进入这种模式后,软件将全屏显示,屏蔽一切干扰,在完成预设的写作目标之前,将无法退出或切换到其他软件。

这个禁止退出的小黑屋,自然也使用了 Kiosk 模式。

Electron 中的 Kiosk 模式

在 Electron 中,将一个窗口设为 Kiosk 模式非常简单,在创建窗口时设置 kisok 属性为 true 即可。

有时,我们的窗口在创建时需要以普通模式显示,然后再在一定条件下切换为 Kiosk 模式,只需用类似下面的代码切换即可:

win.setKiosk(flag)

其中 flag 是一个布尔值。

你还可以使用 win.isKiosk() 方法判断当前窗口是否为 Kiosk 模式。

在实践过程中,我发现很多时候只设置 Kiosk 属性还不太够,还需要设置 frame 等属性。以下是一个示例:

const win = new BrowserWindow({
  // 其他属性...
  closable: false,
  maximizable: false,
  minimizable: false,
  resizable: false,
  fullscreen: false,
  fullscreenable: false,
  frame: false,
  skipTaskbar: true,
  alwaysOnTop: true,
  useContentSize: true.
  autoHideMenuBar: true.
  movable: false.
  thickFrame: false.
  titleBarStyle: 'default',
  paintWhenInitiallyHidden: false,
  roundedCorners: false,
  enableLargerThanScreen: true,
  acceptFirstMouse: true,
  kiosk: true,
  // 其他属性...
})

即使这样设置之后,在 macOS 上有时仍会出现 Docker 栏和顶部系统菜单栏出现在 Kiosk 窗口上方的情况,因此还需要进一步设置 alwaysOnTop 的属性为 screen-saver,代码如下:

win.setAlwaysOnTop(true, 'screen-saver', 1)

在 Windows 和 macOS 中,alwaysOnTop 的窗口有多种极别,按层级由低到高分别是:

  • normal
  • floating
  • torn-off-menu
  • modal-panel
  • main-menu
  • status
  • pop-up-menu
  • screen-saver

如果只是简单地 win.setAlwaysOnTop(true) ,则窗口的级别只是 floating,仍有可能被其他系统组件遮挡。

另外需要注意,在 macOS 下,太高的级别会挡住系统自带输入法的候选字窗口,如果你的 Kiosk 窗口需要用户输入,并且可能使用系统自带输入法的话,这个级别不能高于 modal-panel

一些其他注意点

Kiosk 模式只对当前窗口有效,一个窗口只能覆盖一个屏幕,若用户有多个显示器,则需先检测显示器数量,然后创建多个 Kiosk 窗口分别覆盖。

设置 Kiosk 模式后,用户仍可以使用 Cmd+Q 这样的快捷键退出应用,因此需要在代码中监听窗口的 close 事件,并检查是否处在 Kiosk 状态,如是则阻止退出。代码类似下面这样:

win.on('close', async (e: Electron.Event) => {
  if (win.isKiosk()) {
    e.preventDefault()
    return
  }

  // 其他逻辑
}

Windows 下退出 Kiosk 模式后,窗口的大小可能会变成全屏大小,如希望退出时恢复原大小,可以在进入 Kiosk 模式之前先记住窗口大小,退出后再设置为原大小。

Kiosk 模式并不能阻止用户重启计算机。如果希望重启计算机后能自动恢复 Kiosk 状态,可以将软件设置为随系统启动,并且启动时自动进入 Kiosk 模式。

使用 acme.sh 申请 SSL 证书

2024-09-23 10:57:00

之前很长一段时间,这个博客一直在用云服务商提供的免费 SSL 证书,那个证书有一年有效期,也即一年只需要申请部署一次,因此全手动操作也不算麻烦,但现在免费 SSL 证书的有效期统一缩短为 3 个月了,意味着每 3 个月就要操作一次,这就让手动申请和部署变得麻烦起来了。

最近,我尝试了一下使用 acme.sh 申请 SSL 证书的方法,确实方便了不少,在这里记录一下。

安装 acme.sh

acme.sh 是一个实现 ACME 协议的脚本,主要用途是申请或更新免费 SSL 证书。运行以下命令即可安装:

curl https://get.acme.sh | sh -s [email protected]

更多安装方式可见官方文档:https://github.com/acmesh-official/acme.sh

acme.sh 会被安装在 ~/.acme.sh 目录下。

手动申请证书

安装好 acme.sh 后,可以用以下命令申请证书:

acme.sh --issue --dns -d mydomain.com -d "*.mydomain.com" --yes-I-know-dns-manual-mode-enough-go-ahead-please

记得把其中的 mydomain.com 换成你自己的域名。

上面的代码中,我申请了泛域名证书,所以同时添加了 mydomain.com*.mydomain.com 域名。需要注意的是,*.mydomain.com 不包含 mydomain.com,如果你希望证书除了包含 www.mydomain.com 这样的二级域名,也包含 mydomain.com 的话,记得把 mydomain.com 也加上。

另外,*.mydomain.com 也不包含更深的层级,比如它包含 home.mydomain.com,但不包含 app.home.mydomain.com 。如果你需要更深层级的泛域名,需要把对应的域名也填上。

还需要注意的是最后一个参数 --yes-I-know-dns-manual-mode-enough-go-ahead-please 。acme.sh 更希望用户使用自动申请证书的方式(见下一小节),如果你确实需要手动申请,需加上这个参数,否则命令不会正常执行。

如果一切顺利,acme.sh 命令会输出两段 TXT 信息,需要你手动添加到对应域名的 DNS 解析中,以验证你确实对这个域名拥有权限。在证书申请完成之后,可以删除对应的 TXT 记录。

登录域名服务商(比如阿里云)后台,在域名解析中添加上对应的 TXT 记录,然后再运行以下命令,即可生成证书:

acme.sh --renew -d mydomain.com -d "*.mydomain.com" --yes-I-know-dns-manual-mode-enough-go-ahead-please

证书会被保存在 ~/.acme.sh/ 目录下,包含以下四个文件:

  • mydomain.com.cer 证书
  • mydomain.com.key 密钥
  • ca.cer
  • fullchain.cer 全链路证书

其中在网站场景主要使用 fullchain.cer 文件和 mydomain.com.key 文件。

自动申请证书

可以看到,上面手动申请的步骤,主要的手动操作就是要为域名添加 TXT 记录以验证域名权限,acme.sh 支持让这个步骤自动化,即自动添加 TXT 记录,并在验证完成之后自动删除对应的记录。

以阿里云为例(如果你的域名是在阿里云注册并解析的),首先需要去阿里云控制台获取一个 AccessKey,建议专门设置一个 RAM 用户,只开通 DNS 权限。

得到 AccessKey 之后,在命令行中执行以下命令:

export Ali_Key="key"
export Ali_Secret="secret"

随后再执行以下命令,即可自动申请或更新证书了:

acme.sh --issue --dns dns_ali -d mydomain.com -d "*.mydomain.com"

注意 --dns 参数后面的值为 dns_ali

一切顺利的话,证书申请会自动完成,并被保存在 ~/.acme.sh/ 目录下。

其他各大域名服务商的自动申请方式类似,具体可参见官方文档。

一些注意点

如果你使用了自动申请,AccessKey 会被明文保存在 ~/.acme.sh/account.conf 文件内,如果介意,可在申请完之后修改这个文件并删除对应的 AccessKey。

另外,使用自动申请后,acme.sh 会添加一条定时任务,每天自动检查证书是否需要更新。可运行以下命令查看当前系统的定时任务列表:

crontab -l

现在 acme.sh 默认使用的证书颁发机构是 ZeroSSL,还有一些其他可选机构,比如 Let's Encrypt。可以用 --set-default-ca 修改默认证书颁发机构,比如:

acme.sh --set-default-ca --server letsencrypt

我没有修改 CA,在使用默认的 ZeroSSL 的证书,目前来看暂时没有遇到什么问题。

除了自动申请证书外,大部分网络服务商也支持自动上传 SSL 证书,不过这部分我还没有研究,后续如果觉得值得记录,会另外写文分享。

处理苹果平台的 CONSUMPTION_REQUEST 消息

2024-08-09 16:31:00

最近完善了一下产品的购买流程,其中的一项工作是处理来自苹果 App Store 平台的 CONSUMPTION_REQUEST 消息,在这儿记录一下要点。

消息说明

App 如果使用了苹果的内购(IAP),每当发生用户购买、续费、退款等操作时,苹果服务器都会向开发者指定的地址发送一条消息,不同的消息有不同的 notificationType 值,其中 CONSUMPTION_REQUEST 消息的意思是用户为应用内购买发起了退款请求,App Store 请求开发者服务器提供用户的消费数据,用于协助 App Store 决定是否给用户退款。

开发者可以忽略 CONSUMPTION_REQUEST 消息,也可以根据需要,在 12 小时内回应 App Store。

回应消息

要回应 CONSUMPTION_REQUEST 消息,只需向指定的地址发一个 PUT 请求即可。具体细节可见官网文档

这个 PUT 消息的要点主要有两个:

  1. 在 Header 中添加认证 token 信息;
  2. 在 Body 中发送一个 JSON 格式的对象,向 App Store 提交对应的信息。

数据内容

我们先看 Body 中的数据内容。

根据文档,数据字段以及含义大致如下:

{
    "accountTenure": 0,  // 用户年龄段,0 表示未知
    "appAccountToken": "",  // 用户 uuid,由于之前没有设置,此处留空
    "consumptionStatus": 0,  // 消费状态,0:未知,1:未消费,2:部分消费,3:全部消费
    "customerConsented": True,  // 用户是否同意提供消费数据
    "deliveryStatus": 0,  // 交付状态,0:已成功交付
    "lifetimeDollarsPurchased": 0,  // 用户在应用内购买的总金额,0 表示未知
    "lifetimeDollarsRefunded": 0,  // 用户在应用内退款的总金额,0 表示未知
    "platform": 1,  // 平台,0:未知,1:苹果平台,2:其他平台
    "playTime": 0,  // 用户在应用内的总时间,0 表示未知
    "refundPreference": 1,  // 商家对退款的意见,0:未知,1:支持,2:不支持,3:不确定
    "sampleContentProvided": True,  // 是否已经提供了示例内容
    "userStatus": 1,  // 用户账号状态,0:未知,1:活跃,2:暂停,3:关闭,4:受限
}

你可以根据需要,修改对应字段的值。

请求 Header

请求 Header 中有两个必填的自定义字段,分别是:

  • Content-Type 值固定是 application/json
  • Authorization 值为 Bearer $jwt_token

其中 jwt_token 必须要正确填写,否则请求会返回 401 错误。

jwt_token 的具体生成说明可见官方文档,大致格式类似下面这样:

Header:

{
  "kid": "ZA12345678",
  "alg": "ES256",
  "typ": "JWT"
}

Payload:

{
  "iss": "your_uuid",
  "iat": 1723173620,
  "exp": 1723183620,
  "aud": "appstoreconnect-v1",
  "bid": "your_bundle_id"
}

其中 kidiss,以及生成 JWT 时所需的私钥等几项,需要去 App Store Connect 后台生成。

JWT 私钥

如果你之前还没有生成过对应的私钥,可以前往 App Store Connect 后台的“用户和访问” → “集成” → “App 内购买项目”页面生成,如下图所示:

生成之后,可以在这个页面下载 .p8 格式的私钥。注意这个私钥只能下载一次,下载之后请妥善保存,如果不慎遗失,只能删除再重新生成一个。

上面生成 JWT 所需的 kid 对应上图中的“密钥 ID”,iss 对应“Issuer ID”,私钥即上面下载的 .p8 文件中的内容。

然后就可以用类似下面的方法生成 JWT 了:

import jwt

jwt_token = jwt.encode(
    payload,
    private_key,
    algorithm="ES256",
    headers=headers,
)

最后,将得到的 jwt_tokenBearer $jwt_token 的形式包含在请求头的 Authorization 中,发起 PUT 请求即可。

如果请求返回 202 状态码,表示请求成功了。如果是其他值,可根据错误状态再仔细检查处理。