关于 oldj | 老杰

男,80 后,杭州。阿里 8 年,目前在一个小而美的团队。「妙笔 / WonderPen」「ccReader 」作者。

RSS 地址: https://oldj.net/feed

请复制 RSS 到你的阅读器,或快速订阅到 :

oldj | 老杰 RSS 预览

处理苹果平台的 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 中有两个必填的自定义字段,分别是:

其中 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 状态码,表示请求成功了。如果是其他值,可根据错误状态再仔细检查处理。

在 Flutter 中适配 1Password 登录

2024-06-27 14:34:00

最近在开发 Flutter 项目,其中 iOS 版 App 账号登录时,需要适配 1Password 等密码管理器,即需要告诉 1Password 等密码管理器当前 App 的登录特征信息(域名),以及应该填写界面上的哪些表单项。在这儿记录一下要点。

基本设置

我们首先要处理的,是让 App 和某个域名(通常是官网域名)关联,这样在 App 中唤起 1Password 填写密码时,1Password 才知道应该显示哪些账号。

这儿主要有三个步骤。

Apple 开发者后台设置

在 Apple 开发者后台的 Certificates, Identifiers & Profiles 页面,记得要选中 Associated Domains 选项,如下图所示:

Xcode 中的设置

接下来,要在 Xcode 中为你的 App 添加关联域名,如下图所示:

在 Domains 那一栏,添加 webcredentials:你的域名 即可,比如你的域名是 test.com,那么添加 webcredentials:test.com 就行。

网站设置

最后,还需要在你的域名对应的网站上添加一个认证文件,证明指定 App 确实和当前域名相关。这个文件的文件名固定为 apple-app-site-association,可以放在网站的根目录,或者 .well-known 目录下,确保可以通过网络访问到。

这个文件的用途很多,可能还会包含一些其他字段,和密码管理器相关的主要是以下内容:

{
  "webcredentials": {
    "apps": [
      "TeamId.BundleId"
    ]
  }
}

确保你的 apple-app-site-association 文件包含 webcredentials 字段,并将其中 apps 中的 TeamId、BundleId 换成你的真实 ID。

Flutter 中的设置

为了得到更好的登录体验,Flutter 中也要做一些设置,主要是告诉 1Password 等密码管理器需要填写哪些字段,以及各个字段分别对应什么内容。

关键代码如下:

@override
Widget build(BuildContext context) {
  // ...
  return Container(
    body: Center(
      child: AutofillGroup(
        child: Column(
          children: [
            TextField(
              autofillHints: const [AutofillHints.email],
              decoration: InputDecoration(
                labelText: 'Email',
              ),
            ),
            TextField(
              autofillHints: const [AutofillHints.password],
              decoration: InputDecoration(
                labelText: 'Password',
              ),
            ),
            ElevatedButton(
              onPressed: () {
                // Submit the form
              },
              child: Text('Submit'),
            ),
          ],
        ),
      ),
    ),
  );
}

其中最关键的有两处,一是需要自动填写的表单部分,需要用 AutofillGroup 组件包起来,这样 1Password 就知道哪些字段是需要自动填写的。二是 Email、用户名、密码等需要填写的字段,需要添加 autofillHints 属性,比如 autofillHints: const [AutofillHints.email],这样 1Password 才知道当前字段应该填什么内容。

完成这些设置之后,App 登录时就应该能正常适配 1Password 了。

从打牌想到的

2024-04-21 14:17:00

最近几个月打了很多次牌,有时是线下聚会时和朋友玩,有时则是在手机上玩。玩得久了,逐渐发现扑克牌游戏和现实生活中的规则有一些类似之处。以下是一些感想。

手中的牌

打牌时,手中拿到什么牌非常重要,这一点很容易理解,无论你是高手还是菜鸟,如果起手就拿到一手好牌,那么只要不乱打,并且运气不是差到极点,你基本上都能赢。同样的,就算你是绝顶高手,如果拿到一把烂牌,要赢恐怕也非常难。

当然,拿到极品好牌和极品烂牌的概率都不大,很多时候,我们以及我们的对手拿到的都是中等牌,这种时候,如何组合手中的牌打出最好的效果,就看各自的技术了。

我们的人生也类似,总有一些人一开始就拿到一手好牌,比如家境良好,父母见识不凡,自身也健康聪明,因而只要自己不走错路,人生总体上会非常顺利。还有一些不那么幸运的人,出生在落后的地方贫穷的家庭,几乎没有什么可利用的资源或者助力,要获得成功可就不容易了。

拿到好牌时,不要得意忘形,因为你的成功很大程度源于运气。拿到烂牌时,也不要破罐子破摔,认真思考,尽最大努力打好手中的牌,因为只有这样你才能多一点获胜的机会。

合适的才是最好的

摸牌的时候,一般来说摸到大牌比摸到小牌更好一些,不过稍有经验就会发现,和单独的大牌相比,那些能让现有的牌组合起来的牌可能更好。比如,有一些时候,摸到一张最小的 2 可能会让你的几张散牌组成同花顺,这时对你而言 2 就比大王更好。

生活和工作中也是类似,有一些团队,单独来看每位成员可能都相对普通,但由于配合出色,于是团队整体的战斗力非常强悍。

团队招人的时候,也不是招越牛的人越好,而是要看新来的人能否让团队的整体能力得到提升。有时候,也许加入一位履历一般但却能搞定一些其他人不擅长处理的小事的成员,会让团队整体焕发新生。同样的,一位看似不重要的成员离开,也有可能打断团队内部的某种连接,让团队效率大受影响。

寻找人生伴侣也是如此,那些光彩夺目的潜在选项当然也不错,但一位能与你互补,让你成为更好的自己的伴侣,也许是更好的选择。

本钱

拿到什么牌很大程度上能决定你单局的胜负,有多少本钱则决定你能在牌桌上待多久。

以腾讯欢乐斗地主的掼蛋游戏为例,新手场单场输赢封顶是 5 万欢乐豆。即如果你手中的欢乐豆不足 5 万,那么你赢的时候手中的欢乐豆翻倍,输的时候会赔得精光。当然,实际上输赢的数额还取决于对手的豆子够不够,不过为了简化讨论,此处我们假设对手总是有足够的豆子。

这个规则很容易理解,而且看起来似乎也很公平。但在玩了很多场之后,我发现这其实是一个对水平普通且本钱较少的玩家非常不友好的规则。

假设你是一位普通水平的玩家,即你的水平处在中游,每局有 50% 的概率赢,也有 50% 的概率输,并且初期你只有 1000 欢乐豆,你会面临什么情况呢?

当你的豆子数量在 5 万以下时,无论你之前赢了多少次,只要输一次,你的豆子就被清空了。也就是说,想要让自己的豆子超过 5 万,你需要连赢 \(6\) 次,才能让自己的豆子从 1000 增长到超过 5 万的数量(\(1000 \times 2^6 = 64000\))。但是,由于你的赢率只有 50%,因此连赢 6 次的概率也只有 \(\frac{1}{2^6}\),即 \(\frac{1}{64}\) 或者 1.56% 。

但即使你有了 6.4 万豆子,你仍然不安全,只要连输两次(发生的概率为 \(\frac{1}{4}\) 或 25%,这并不是一个小概率),你就又会回到一无所有的状态。要让自己更安全一些,你可能需要至少 20 万豆子,这样只有连输四次(发生的概率为 \(\frac{1}{16}\) 或 6.25%),你才会输光所有豆子。而要从 1000 豆子变成 20 万豆子,你需要先连赢 6 次,然后在接下来保住本金的情况下再净赢至少 3 次。

粗略计算一下就可以知道,作为一名赢率只有 50% 的中等水平玩家,取得这个成就的概率不足 1%。

另一方面,如果你仍然只是一名中等水平的玩家,但你已经有 20 万甚至更多的豆子了,会怎么样呢?

由于你的输赢概率各一半,并且每次输赢都只是增减 5 万欢乐豆而不是一下子输光所有,因此你的账户余额从概率上来讲将会一直持平。期间可能会因为随机因素出现一些波动,甚至有非常小的概率你在某次大波动中余额被击穿从而破产,但更大的概率是你的资产经过波动之后又回归到初始值附近。

换一句话来说,对一名中等水平玩家来说,从底层逆袭的成功概率极小,因为在初期他需要连赢很多次才行,同时只要输一次就会完全失败。但同样水平的玩家,如果一开始就拥有大量本钱,他却有很大的概率能守住这些财富,因为他不怕输,即使输了也仍然有机会赢回来。也即本钱越少,越不容易出头,财富越多,则越容易守住财富。

这大概也是阶级固化的一种解释吧。

读《星震》

2024-03-08 16:50:00

科幻小说《星震》是《龙蛋》的续集,讲的是人类造访一颗名为龙蛋(也叫蛋星)的中子星,行程结束准备离开时,蛋星发生了一场星球级别的大地震(星震),星球上的奇拉文明几乎毁灭,随后奇拉与人类相互帮助、相互拯救的故事。

读这部小说之前,最好先读过前作《龙蛋》,否则对故事的背景可能会难以理解。在继续写这篇读后感之前,也让我们先回顾一下《龙蛋》的前文提要以及设定。

蛋星是一颗由比太阳还要大的红巨星坍塌而成的中子星,直径只有二十公里,因此星球上的物质密度极高,引力也极为强大,根据书中的设定,蛋星表面的引力强度是地球上的 670 亿倍!

在这种环境下,蛋星上物质的形态与地球上常见物质的形态自然大不相同,那儿的各种活动不再依赖分子级别的化学反应,而是速度更快的核子级别的物理反应,于是对那颗星球上的智慧生命奇拉而言,它们的时间比我们快 100 万倍。所以,在前作《龙蛋》中,人类刚与奇拉们相遇时,奇拉们还处在石器时代,但一星期后,奇拉们的科技就已经远超人类。

然而,就在人类基本完成考察准备离开时,一场灾难突如其来,星震发生了,蛋星上的奇拉文明遭受了毁灭性的打击,无数奇拉死去,大量建筑和设备报废,只有少数幸运儿活了下来,但他们几乎不会操作和修复那些先进的设备。

不幸中的万幸是当时蛋星上空还有一些正在太空中执行任务的奇拉,这些太空奇拉受过良好的教育,依然掌握着部分先进技术,它们是奇拉恢复文明的希望。但问题是,地表的降落场地都已经在星震中被毁坏了,他们无法降落,只能先设法联系上地表的奇拉,并远程指导他们修复降落场。

一开始,一切都很顺利,但不久之后,地表上一些原本处在社会边缘的奇拉有了其他心思,他们觉得现在这种没有法律可以随心所欲的灾后世界似乎也挺好,如果让太空奇拉降落,他们是不是又要回到从前那种处处受限的状态?

一番争斗之后,这些“坏”奇拉赢了,他们建立了新的秩序,蛋星的地表文明退回了蛮荒时代。太空奇拉无法降落,只能另想办法,而唯一的办法可能会让正准备离开的地球人处于危险之中……

最后,在太空奇拉、地表“好”奇拉以及人类的共同努力以及牺牲之下,他们终于重建了文明,而且在 100 万倍的时间流速下,很快就达到了比之前更高的高度。

整个故事对人类而言都是在一天之内发生的,但对奇拉而言却已经过去了很多代人。

比较有意思的是故事中没有一个贯穿始终的超级英雄个体,很多奇拉在为恢复文明而努力,某位奇拉在某个阶段出场很多,正当你以为 Ta 是主角时,Ta 却可能突然因为意外或战争“流逝”(故事中奇拉的死亡被称为“流逝”)了。这种安排可能会让期待超级英雄的读者读起来不那么“爽”,但掩卷细思却也觉得非常合理,因为他们面临的是星球级别的灾难,在这种灾难面前个体的力量是渺小的,只有所有人合力,一起向着一个方向前赴后继地努力才有可能取得最后的成功。

故事中也探讨了一些科幻作品中常见的问题。比如,科技文明的诞生是必然吗?作者认为并不是,故事中作者借一位奇拉的思考表达了这样的观点:蛋星上的奇拉们虽然很早就有了智慧,进入了原始文明,但如果不是人类飞船的造访和影响,也许奇拉会继续在低水平的文明中徘徊漫长时光。而如果在星震发生的时候,他们仍然没能发展到可以进入太空,那么迎接他们的将会是一场更彻底的倒退甚至毁灭。

另一个问题是,奇拉文明的科技水平已经远远超过了地球文明,为什么他们没有入侵或者消灭地球文明?故事中也给出了解释,因为奇拉文明是中子星文明,由普通物质组成的地球世界在他们眼中几乎是空的,太阳系的资源对他们来说也几乎毫无用途,换句话说就是他们和地球文明处在完全不同的生态位上,没有竞争关系。做一个不太恰当的类比的话,奇拉文明和地球文明就好像狮子和野草一样,两者所需的资源差异极大,又对彼此都没有威胁,因此完全可以和平共处。如果奇拉文明遇到另一个中子星文明,或许就不会如此和谐了。

还有一个经常出现在科幻作品中的问题是,为了活下去,我们可以吃同类的尸体吗?故事中奇拉文明和人类文明有着完全不同的道德观和选择,对奇拉文明而言这根本不是一个问题,因为在他们的文化中吃死去同类的尸体是很正常的事,即使在不缺食物的情况下,同类的尸体仍然是他们喜爱的美食,甚至宴会中最好的菜可能就是一份烤奇拉。但对地球文明来说,这却是让人难以接受的事,故事中地球人类遇到这个困境时,果断拒绝了这个选项,宁可饿死。

在本书的最后,作者还描写了奇拉文明中的一些细节,有一个细节我觉得挺有趣:奇拉文明中的建筑都是没有房顶的。

为什么会这样?作者解释说因为蛋星的引力太大了,加上大气稀薄(蛋星上的大气由电子、铁离子或其他典型星壳核子组成),因此自然界中从来没有进化出会飞的生物,大概也没啥能跳得很高的生物,于是他们的世界基本上是二维的,几乎从来不会有麻烦来自上方,所以他们只需要建四面墙把自己围起来就安全了,不需要屋顶。

总的来说,这是一部有趣的硬核科幻小说。

不要做“微操”型管理者

2024-02-23 20:52:00

工作多年,被人管过也管过人,有一个不算新颖但深有感触的心得,即管理者应该要尽量避免“微操”。

“微操”一词源自游戏,指玩家对游戏中的单位进行非常细微的操作,比如在即时战略游戏中,非常细致地控制每个士兵的行为。

对一些游戏来说,“微操”是褒义词,表明玩家的水平非常高,能发挥出各个单位最大的战斗力,这很好理解,因为游戏中的单位通常智能程度不高,同时总是能不折不扣地执行玩家的指令。但在现实世界中,“微操”则通常不是什么好事。

我曾有一段比较痛苦的工作经历,当时的上司就非常喜欢“微操”,细微到产品的每个设计细节都要管,比如每个按钮的位置和文案、每个输入框的样式以及每个操作的流程他都要发表意见。

他的能力确实很强,在业内小有名气,也有过几个成功的项目,因而十分自信强势,加上有着上司的身份,在团队内威信极高,对产品的各个细节都有着最终裁决权。

刚加入团队时,我非常有干劲,尽心尽力地投入产品的设计和开发,但一段时间后,我发现我提交的方案常常会被他改动很多地方,而有时一个看似很小的改动却可能意味着之前的大量工作需要推倒重来。

一开始我陷入了自我怀疑中,是不是我的能力真的太差了?不然为什么有这么多细节不符合上司的要求?我尝试努力总结上司的各个修改逻辑,但大部分修改我实在看不出有什么规律,对我来说,哪些地方会被打回似乎是完全随机的。

反复的细节修改以及确认,让我的工作量增加了很多,精神上也疲惫不堪。

终于有一天,在再一次无奈的修改之后,我决定摆烂了。由于无法预测哪些方案可能会被打回修改,我便尽可能地不做决定或者少做决定,每一个我觉得有被打回风险的设计都尽量先去问一问他。

摆烂之后,我果然轻松了不少,很少再遇到方案被打回或者需要大量修改的情况了,因为实际上我只是在做一些不太需要动脑子的执行工作,需要决策的部分都扔回给了上司。

但这显然不是正常的或健康的状态,一段时间后,上司发现自己越来越累,于是变得愤怒,开会时常常斥责我们没有达到他的期望。我也很愧疚,尝试再次担起责任,但当我再次深入方案设计然后继续遭遇大量细节被打回后,我彻底摆烂了,并且在不久之后离开了那个团队。

后来我回顾那段工作经历,逐渐意识到了其中令彼此痛苦的根源。那些被打回修改的细节设计,真的都有必要,或者都有对错之分吗?

不可否认,上司提出的一些意见确实更好,遇到这样的修改要求时,相信任何一位合格的员工都会接受,并且心悦诚服。但在那段工作经历中,从数量上来说这样的修改只占少数,更多的则只是一些纯粹的偏好性质的改动,比如一些文案的表述方式,几个元素是左对齐还是居中对齐,一些操作流程的顺序安排等等。在上司介入的阶段,我们的方案已经经过了反复的思考和讨论,通常来说已经没有明显的问题,但上司总是能提出很多修改意见,而在我们看来这些意见和原方案相比并没有明显的优劣或者绝对的对错,无论哪种选择都有其道理,但说到底,很多方案的差异其实只是口味或习惯的不同罢了,就像有人喜欢川菜,有人喜欢淮扬菜,因此在烹饪时流程和用料不一样,仅此而已,但只要在整体上保持风格一致,做出来的就不失为一道好菜。

每个人都有自己的偏好,除非他人的思维习惯和你完全相同,否则他的方案和你预想的方案多多少少总是会有差异。员工如果是新手,管理者手把手地指导自然没有问题,但这显然不应该成为常态,当员工成长起来,有足够的经验之后,作为管理者,我想只要大方向没有问题,细节上应该允许下属自由发挥。这也是所谓的“抓大放小”、“充分放权”。

况且很多时候,作为一线员工,在产品方案上他们了解的信息可能比管理者更多,很多方案可能已经是综合考虑体验和成本之后的结果,一个看似不起眼的“小”修改意见,可能会需要几个小时甚至几个人日的工作,而连着几个“小”意见,则可能意味着成本的飙升。

喜欢“微操”、坚持按自己的喜好指挥每一个细节的管理者,从本质上来讲,要么是对员工不信任,认为员工没有能力做好工作,要么则是沉迷于操纵一切的掌控感,不愿意放权。无论哪种情况,对团队来说都是有害的。

如果管理者真的认为某个设计或者细节很重要,必须严格执行,那么最好的方式是在一开始就清晰地向员工说明,而不是等到方案都已经基本完成时才不断提出各种“重要”修改意见。

如果员工自己的创意总是被管理者修改甚至否定,那么逐渐地,员工就会放弃主动,——既然自己没有决定的权力,那么就干脆做个不动脑筋的执行者吧。最终,员工不开心,没有足够的参与感和成就感,成长有限;管理者也不开心,觉得招了一帮废物,什么都要自己来决定,让自己都没有时间去做更重要的工作了,却没有意识到造成这一切的根源可能就是自己。

在团队中,管理者天然有着更大的权力,合格的管理者应该学会控制自己的欲望,合理地使用这个权力。很多时候,并不是管得越多越细就越好,作为管理者,在准备提出意见时应该先想一想,这是必要的吗?

怀念爷爷

2024-01-07 11:40:00

爷爷生于 1924 年,今年是爷爷诞辰 100 周年。

1

爷爷受过良好的教育,解放前当过老师,在山村小学教书育人,也曾一腔热血,保护过被敌人追捕的游击队员。解放后,爷爷先在县法院工作,后转向水利、航运方向,参与主导了很多复杂的工程,并逐渐成为这个领域的知名专家。

1970 年代,云南省开发澜沧江,爷爷以专家身份被聘为技术指导。那个年代的工作条件非常艰苦,在一次位于西双版纳的工程作业中发生了意外,奋战在一线的爷爷不幸负伤致残,从此不得不离开热爱的事业,提前退休回到家乡。

2

从我有记忆起,爷爷就是一个整天坐在客厅藤椅上的老头,很瘦,很慈祥,从未见他发过脾气,对谁都很和蔼。

爷爷的伤在腿上,这个伤让他失去了自由行走的能力,因此他只能天天宅在家,极少出门。

爷爷有一大箱子书,我曾好奇地翻过,但那时我还不识字,只依稀记得书中有很多复杂的图表。

有时会有一些同样头发花白的人来看爷爷,爷爷会很高兴地与他们聊很久。

没有客人时,爷爷会看书看报,还会教我唱歌,但更多的时候他就那么静静地坐在那儿,不知是在回忆还是思考。

3

也许是长年的坐卧损害了健康,爷爷去世得很早。

爷爷弥留之际被大伯接了过去。有一天母亲带我去大伯家看爷爷,我进了屋,看见爷爷正躺在病床上,见我进来,就睁大了眼睛看向我。

年幼的我还不理解死亡的含义,只觉得爷爷睁大眼睛的样子很有趣,还以为他马上就要像以前一样逗我玩了,于是嬉笑着跑了出去。

却没有意识到,那就是我和爷爷的最后一面。

4

爷爷虽然脾气温和,却是家里的定海神针。他去世一年之后,我们举家搬迁去了外省,从此故乡对我而言成为了遥远的回忆。

5

多年后的一天,回到老家大伯家小聚,期间大伯娘指着我感慨地说:“其实他最像爹。”

回想起爷爷的照片,我这才发现,原来不知不觉间,我竟真的有了几分爷爷的样子。

也谈西方伪史论

2023-12-31 12:55:00

最近看了一些西方伪史论的言论,觉得很是荒诞,也写一点看法。

所谓西方伪史论,大体上就是认为西方的历史(部分甚至大部分)是伪造的,更深一层的思想则是认为西方也不过尔尔,没什么了不起的。

关于西方伪史的言论,一开始是一些人质疑西方历史中可能有造假的部分,这还属于正常的探讨。接着,一些人继续推进,认为古希腊的哲人比如亚里士多德等都是假的,流传下来的著作都是后人的伪作,这时的种种论点就有点经不住推敲了。再接着,既然西方那些先贤不存在,那么那些思想是哪里来的呢?有人就说全部源自古代中国,是被西方偷走的,更有甚者认为《几何原本》也是中国古人写的,现代科学也是中国古人发明的,还例举了种种所谓的“证据”,将西方文明贬得一无是处,同时将中国古代文明无限拔高。这些言论荒诞不经,错漏百出,但大概听着让人高兴,于是附和者甚多。

那些西方伪史论的鼓吹者,不知是真的相信这套理论,还是只是为了利益或者吸引流量故意语出惊人。不过,显然他们并不是最早这么做的,西方伪史论并不是什么新奇的东西,一百多年前,严复先生在《救亡决论》中就曾写道:

“晚近更有一种自居名流,于西洋格致诸学,仅得诸耳剽之余,于其实际,从未讨论。意欲扬己抑人,夸张博雅,则于古书中猎取近似陈言,谓西学皆中土所已有,羌无新奇。……

“……尤可笑者,近有人略识洋务,着论西学,其言曰:「欲制胜于人,必先知其成法,而后能变通克敌。彼萃数十国人才,穷数百年智力,掷亿万赀财,而后得之,勒为成书,公诸人而不私诸己,广其学而不秘其传者,何也?彼实窃我中国古圣之绪余,精益求精,以还中国,虽欲私焉,而天有所不许也。」有此种令人呕哕议论,足见中国民智之卑。”

也就是说,早在晚清时期,“西方伪史论”、“西方科技是从中国偷的”这类言论就已经存在了,现在那些动机可疑的鼓吹者们也不过是拾人牙慧,却偏偏一个个以“独立思考”、“世人皆醉我独醒”的姿态在网络上大放厥词。

近代以来,西方领先世界已有数百年,这期间尤其是最近一百多年里,世界各国有无数学者穷尽毕生精力,不断研究总结西方领先的秘密,各类学说、著作汗牛充栋。倘若真如西方伪史论者所说,西方的科技都是从中国偷的,如此大规模的偷窃不可能没有痕迹留下来,为何一百多年来从未有人给出让人信服的证据?西方世界并不是一个整体,内部也常常互相拆台,甚至多次打得头破血流,并非铁板一块,难道他们偏偏就能在“科技是从中国偷的”这个话题上统一守口如瓶,这么多年没能让外人找到丝毫证据?

晚清时期,中华持续数千年“天朝上国”的地位被打破,国家和民族跌入深渊,这种心理上的强烈落差让人很难接受,人们迫切需要一些理论能让他们在面对西方文明时能保持一些心理优势,于是,西方伪史论应运而生。

现在我们则又走到了另一个重要的历史节点,经过无数国人的艰苦奋斗,中华文明再度崛起,已到达重回世界巅峰的前夜,但由于此前上百年落后的阴影实在太大,很多人面对西方时仍然不够自信,他们需要一种理论,证明我们配得上复兴后的地位,于是西方伪史论便再次有了土壤。

中国古代有过辉煌,留下了无数光彩夺目的思想和成果,但我们也不可否认,在近代我们落后了,西方文明有其值得学习之处。

我们确实需要重建自信,但这种自信应该实事求是,而不是建立在臆想或者对对手的无脑贬低之上。我们祖祖辈辈都在这块土地上奋斗,我们强大过,也落魄过,现在,我们通过数代人勤奋踏实的努力再次找回了一度丢失的东西,并且我们知道,即使再遭遇挫折我们也有勇气以及韧性重新站起来,这才是我们自信的底气。

而那些夸张荒诞的西方伪史论,或许能兴起一时,博得若干眼球,却终将贻笑大方,被扫入历史的垃圾堆。

增加新的柜员后会发生什么?

2023-12-19 11:09:00

看到一篇有趣的短文《增加新的柜员后会发生什么?》,讲的是排队论。

银行柜员问题

文章中讨论了这样的一个问题:

假设一家小银行只有一名柜员,每位顾客平均需要 10 分钟的服务时间,每小时预计有 5.8 位顾客到达,那么预计每位顾客的等待时间是多少?

乍一看之下,每小时有 60 分钟,但只有 5.8 位顾客到达,即平均每 10.3 分钟才有一位新顾客到达,同时每位顾客平均只需要 10 分钟的服务时间,似乎一位柜员就足够服务好这些顾客了,每位顾客都不用等待或者不会等待太久?

但事情显然不会这么简单,假设顾客到达的时间和所需的服务时长都是随机的,那么每位顾客在得到柜员服务之前,平均需要等待近 5 小时之久!

为什么会这样?其实稍加思考,我们就能发现端倪,因为那个 10 分钟只是平均时间,但每位顾客所需的实际时间差异可能会很大,有人可能只需要一两分钟,有人则可能会需要几十分钟甚至几小时,而一旦遇到一位需要长时间服务的顾客,所有后来的顾客都只能排队等待。同时,顾客的到达时间间隔也不是严格的 10.3 分钟,有时可能几十分钟甚至几个小时都没人来,有时则一下子连着来了好几位顾客。

那么,如果增加一位柜员会怎么样呢?

你或许会以为,增加了一位柜员后服务处理能力加倍,那么顾客的平均等待时间也应该对应地减半,平均每位顾客大概只需要等待两个多小时吧?但事实上,增加一位柜员后,顾客的平均等待时间会一下子下降到 3 分钟,仅为原来的 \(\frac{1}{93}\)。

也就是说,只有一位柜员时,顾客的平均等待时间可能会长得难以接受,但增加一位新的柜员之后,顾客的体验就相当良好了。

原因也很简单,当有两个柜台同时提供服务时,如果一个柜台被阻塞了,顾客可以转向另一个柜台,而不用在后面干等。当然,也存在连着来了两位需要长时间服务的顾客的情况,这时两个柜台都被阻塞,新顾客仍然需要长时间等待,但在上面讨论的这个场景中,这种情况发生的概率很低,对等待时间的平均值影响非常微小。

泊松分布和指数分布

在统计学上,顾客的到达时间一般认为符合泊松分布,顾客所需的服务时长一般认为符合指数分布,两者都是非常常见的分布。

泊松分布通常用于描述在固定时间或空间内随机发生的离散事件的次数。比如上面例子中,不断有顾客进入银行,具体什么时候有顾客进入是一个随机事件,无法预测,但我们可以估算“指定的一段时长(比如 1 小时)内有 \(n\) 位顾客到达”的概率 \(P\) 是多少,公式如下:

\[P(X=n)=\frac{e^{-\lambda}\lambda^{n}}{n!}\]

其中 \(\lambda\) 表示事件发生的频率,这个频率是通过统计之前收集的数据得到的,比如本例中我们已知每小时预计有 5.8 位顾客到达,那么以 1 小时为单位,\(\lambda\) 即是 5.8。

根据这个公式,可以计算出 1 小时内有 n 位顾客到达的概率,当然,n 在 5.8 左右时概率最大,越偏离 5.8 概率越小。比如 1 小时内有 100 位顾客也是有可能的,只是这个概率会小得可忽略不计。

以下是一些常见的泊松分布的事件:

指数分布和泊松分布可以认为是描述相同现象的不同部分:泊松分布关注一段时间内发生 n 个随机事件的概率,而指数分布关注每两个连续的随机事件之间的时间间隔。

指数分布的公式也与泊松分布的公式相关。比如,如果想计算一段时间内某件事不发生的概率,就相当于计算上面泊松分布中 \(n=0\) 的概率:

\[ \begin{align*} P(X=0)&=\frac{e^{-\lambda}\lambda^{0}}{0!} \\ &=e^{-\lambda} \end{align*} \]

而在一段时间内事件发生的概率,则是 1 减去上面的值,即:

\[1-e^{-\lambda}\]

以下是一些常见的指数分布的事件:

项目管理中的排队论

排队论在各种服务性质的工作中非常有用,除此之外,软件开发中也有很多场景可以参考。

软件的开发和维护过程与柜台服务类似,不断地有新的需求(顾客)过来,程序员(柜员)则负责处理这些需求。新需求的到达时间以及工作量都是随机的,可以大致认为分别符合泊松分布和指数分布。

团队的管理者常常会期待这样一种理想状况:每一个需求过来时都能很快得到处理,同时员工人员基本没有冗余(即没有人闲着)。但只要在软件开发团队待过就会知道,这种理想状况极少发生,大多数时候都是需求不断积压,程序员加班加点仍然难以赶上进度。

如果你仔细地测算每一个需求的工作量,以及平均每周有多少新需求,你可能会发现如果只看平均值数据,团队应该能够处理这些工作才对,但事实却是很多需求都延期了。看了上面的讨论,你现在应该知道了,很多时候工作安排并不能简单地算平均值,因为需求具有随机性,如果你希望对抗这种随机性,让新需求能尽快得到处理,一些冗余将是必须的。

当然,软件开发和柜台服务之间也有很大的不同,其中之一是开发人员通常不能简单地互换,和柜员相比,开发人员之间交接工作的成本可能会比较高。不过总体来说,排队论的思想和方法仍然是适用的。

Electron 使用 OV 代码签名证书

2023-08-01 12:00:00

去年写过一篇 Electron 在 Windows 下的代码签名,今年 7 月中旬,这个证书要到期了,准备续一下,结果发现现在已经不再新签发纯数字代码签名证书了,要进行代码签名,必须购买带 U 盾硬件的版本。和原来的数字版相比,U 盾版贵了不少,而且由于 U 盾需要邮寄,还需要额外的邮费,花费的时间也比数字版多了很多。

U 盾证书的使用和纯数字证书有一些不同,下面记录一下主要过程。

购买证书

购买过程就不详述了,有很多服务商,比如 DigiCert、Sectigo、SSL.com、CheapSSLSecurity 等,当然,也可以在淘宝上找代理。

我是通过淘宝代理购买的 Sectigo 的证书,因为比较了下这样最划算。证书是由 Sectigo 官方从加拿大寄过来的,我下单时由于厂商芯片缺货,等待了 21 天才发货,不过发货之后就快了,从发货到 U 盾到手只过了 4 天。

下图是我收到的 U 盾的样子。

准备工作

由于我的主力开发机器是 M1 芯片的 MacBook Pro,我希望能直接在 mac 上使用这个 U 盾,而不要在打包签名时换电脑。一番探索之后,发现是可以实现的,主要要点如下:

接下来,需要在虚拟机中的 Windows 11 上安装 SafeNet Authentication Client (SAC) 软件,一般可从证书颁发商网站或者发你的邮件中找到下载链接。

安装之后,插上 U 盾,如果在 SAC 软件中看见了你的设备,就表示一切正常,如下图所示。

证书颁发商发来的邮件中包含了初始密码,可在这个软件中修改密码。注意保管好新密码,万一不慎遗失会比较麻烦。

安装证书

接下来,需要将这个证书安装到当前 Windows 系统中。

点击 SAC 界面中的高级视图:

找到证书,双击安装即可,如下图所示:

另,SAC 软件也有 macOS 版,但这个证书似乎无法安装在 macOS 上。即使安装了应该也没有用,因为后面签名时仍然会调用 Windows 中的证书。

在 Electron 中使用

我使用的是 electron-builder 进行的打包、签名,主要库以及版本号如下:

其中 electron-builder 的关键配置如下:

win: {
  // ...
  verifyUpdateCodeSignature: true,
  signingHashAlgorithms: ['sha256'],
  signAndEditExecutable: true,
  signDlls: false,
  certificateSubjectName: 'YOUR_NAME',
  publisherName: 'YOUR_NAME',
  rfc3161TimeStampServer: 'http://timestamp.sectigo.com',
  timeStampServer: 'http://timestamp.sectigo.com',
},

将其中的 YOUR_NAME 换成你证书中的名字即可。

问题处理

签名过程中,我遇到了下面的错误:

ensure that 'Share folders' is set to 'All Disks', see https://goo.gl/E6XphP
Error: Exit code: 2. Command failed: prlctl exec {e28c7c00-0805-4729-91e9-3bfeacdbbbf8} --current-user \\Mac\Host\\Users\wu\Library\Caches\electron-builder\winCodeSign\winCodeSign-2.6.0\windows-10\arm64\signtool.exe sign /tr http://timestamp.digicert.com /sha1 C2F9D1106A2D69E9351CCE1EF1B6E1AA3E9C475B /s My /fd sha256 /td sha256 /d ......

研究了一会儿,发现是因为我的 mac 是 M1 芯片,装的 Windows 11 虚拟机也是 Arm 版的,而目前 winCodeSign 工具还没有提供 Arm 版的签名工具,于是命令失败。

解决办法很简单,导航到 mac 的 ~/Library/Caches/electron-builder/winCodeSign/winCodeSign-2.6.0/windows-10/ 目录下,把 x64 目录复制一份,并重命名为 arm64 即可。

接下来,应该就能给 Electron 的 exe 文件顺利地签名了。

改进

签名虽然能顺利完成,但过程中却会多次出现密码输入框,如下图所示:

这个密码输入过程不但烦琐,还让命令无法自动完成,因为每次流程都会被卡住,需要手工输入密码。

按理这个地方应该有更优雅的做法,electron-builder 的文档中也提到了 certificatePasswordCSC_KEY_PASSWORD 等参数,但我试了多次却总是不能生效。

最后,我写了一个简单的 AHK 脚本解决了问题。脚本内容如下:

Loop
{
    If WinExist("设备登录", "令牌密码")
    {
       WinActivate
       Sleep 500
       Send "{Raw}YOUR_PASSWORD"
       Send "{Enter}"
    }
    Sleep 1000
}

注意将其中的 YOUR_PASSWORD 替换为你的密码。当然,你也可以将密码保存在环境变量中,然后在 AHK 脚本中从环境变量读取。

小结

U 盾代码签名证书的购买、使用都比之前的数字证书要麻烦,不过为了提升用户下载、安装软件的体验以及安全性,这个证书还是值得的。

这个签名证书需要安装在 Windows 中,不过 macOS 上的 Parallels 虚拟机也可以。

一些相关事项也可以看我一年前写的数字证书的用法

Discourse 集成 Django 登录

2023-06-27 09:41:00

最近搭建了一个 Discourse 论坛,尝试使用另一个已有的 Django 系统中的账号进行登录。踩了一些小坑,下面记录一下过程。

本文内容基于以下软件以及版本:

安装 Discourse

Discourse 的安装比较顺利,基本上按官方说明操作即可。最好使用一台没有 Web 服务的机器,即 80 和 443 端口没有被占用,否则会有一些麻烦。

域名我是通过 Cloudflare 管理的,一开始一直遇到“ERR_TOO_MANY_REDIRECTS”错误,查询之后发现是因为“SSL/TLS 加密模式”需要使用“完全”模式,不能使用默认的“灵活”模式。

邮件发送服务,先是试了下阿里云的邮件推送,后来又根据 Discourse 安装说明页面的推荐,选了 Mailjet 的服务。阿里云邮件推送和 Mailjet 都可以支持,且两者每天都有 200 封免费额度,对小应用来说足够了。

Discourse 准备

Discourse 内置了 Google、Facebook、GitHub、Twitter 等第三方登录,只需在设置中配置开启即可。不过如果想集成自定义 OAuth2 登录服务,还需要安装一个由官方维护的额外插件 discourse-oauth2-basic

插件安装方式为修改 Discourse 服务器上 /var/discourse/containers 目录下的 app.yml 文件,找到其中 hooks 字段,将插件的 git 仓库地址添加到 cmd 中,如下面的代码所示:

hooks:
  after_code:
    - exec:
        cd: $home/plugins
        cmd:
          - git clone https://github.com/discourse/docker_manager.git
          - git clone https://github.com/discourse/discourse-oauth2-basic.git

注意其中的 docker_manager.git 是默认有的,最后一行的 discourse-oauth2-basic.git 则是新添加的。

添加完成之后,在 /var/discourse 目录下执行以下命令:

./launcher rebuild app

命令的执行过程需要几分钟,完成之后新的配置就生效了。

Django 准备

Django 默认不支持 OAuth2 登录,不过有一个成熟的第三方插件 Django OAuth Toolkit 解决了这个问题,根据官方文档安装即可。

安装完成并在 settings.py 中引入 Django OAuth Toolkit 之后,记得添加下面一项配置:

OAUTH2_PROVIDER = {
  # 其他配置项...
  'PKCE_REQUIRED': False,
}

Django OAuth Toolkit 默认启用 PCKE,但目前 Discourse 的 OAuth2 插件还不支持 PKCE,如果不把 Django 中的 PKCE_REQUIRED 设为 False,后面登录时可能会遇到 invalid_request 错误,具体错误描述为“Code challenge required.”。

另外,也需要为 Django OAuth Toolkit 服务指定一个访问地址,比如在站点的 url.py 中添加类似下面的记录:

path('oauth/', include('oauth2_provider.urls', namespace='oauth2_provider')),

之后便可以通过 https://YOUR_DJANGO_SITE/oauth/* 访问 OAuth2 接口。

最后,还需要在 Django 站点中准备一个页面,可以使用 OAuth2 授权访问,页面内容为以 JSON 格式显示的用户信息,以便 Discouse 登录成功后读取用户名、Email 等信息。

Django 配置

在 Django 管理后台的“Django OAuth Toolkit” → “Application”中新添加一条记录。各项填写要点如下:

其余没有提到的项使用默认值即可。

Discourse 配置

最后,再回到 Discourse 这边的配置。

在 Discourse 的插件管理页面,可以看到安装的插件列表,点击 discourse-oauth2-basic 插件的设置按钮可进入对应的设置页面。

当然,你也可以在总设置的“登录”面板下看到相关的设置,不过那个面板选项很多,如果只想设置 OAuth2 相关的选项,可以点击插件的设置按钮以过滤掉无关项。

以下是主要设置项以及说明。

其中 oauth2 authorize urloauth2 token url 和你的 Django 配置有关,如果你配置的 OAuth2 服务的前缀为 /oauth/ ,则对应的地址如下:

# oauth2 authorize url
https://YOUR_DJANGO_SITE/oauth/authorize/

# oauth2 token url
https://YOUR_DJANGO_SITE/oauth/token/

之后的 oauth2 token url method 选择 POST,再之后的 oauth2 callback user id pathoauth2 callback user info paths 留空或使用默认值即可。

再下面则是 oauth2 fetch user details 相关的字段,你需要先在 Django 站点准备好一个页面,比如页面为 https://YOUR_DJANGO_SITE/api/account/info,返回内容形如:

{
  "data": {
    "uid": "xxx",
    "username": "my_name",
    "email": "[email protected]"
    // ......
  }
}

还可以添加更多字段,比如用户全名、头像图片地址、Email 是否已验证等等。

随后,将相关字段填写到接下来的字段中即可。比如:

其余字段根据需要填写即可。

如果一切顺利,你的 Discourse 站点就可以使用 Django 站点的账号登录了。