2025-03-21 00:00:00
有没有这样一种奇妙体验:家里老人对你的忠告嗤之以鼻,却对网络医疗广告深信不疑?仿佛那些”三天速效”“纯天然”“祖传秘方”字样自带某种魔力,能让他们心甘情愿掏空钱包?
我爸就是这样。医院检查出肠道息肉后,他不愿接受正规治疗,却在网上找了个不知名的乡镇医院,买了一堆贵得离谱的中药。当我劝他去本市三甲医院时,我们吵了一架,不欢而散。
我百思不得其解:为什么长辈会信任网上随机广告,胜过亲生儿女的劝告?是年代差异让他们天然信任媒体?是搜索引擎大品牌的背书效应?还是他们骨子里相信”酒香也怕巷子深,神医总在小诊所”?
曾经,我尝试给他安装丁香医生,希望提供专业的医疗参考。结果他嫌上面评论太少,不够可信。转头却给我展示抖音上的”体外无痛胃肠检查”广告——零评论,明显广告标识。我内心:这双标技术堪称奥运冠军水平啊!
绝望之际,我发现了转机。
一天晚上,我爸向我展示他用”豆包”AI制作的视频。灵光乍现!我让他用豆包查询那个”神奇”的体外检查技术。豆包给出了客观分析,而他竟然接受了这个意见!
第二天他主动用豆包查询其他健康问题,我立刻顺水推舟:”以后不用上百度了,直接问豆包就好。”他居然欣然接受!
所以,我所谓的方法其实很简单:用 AI 助手替代浏览器和搜索引擎。无论是豆包、DeepSeek 还是其他 AI 产品,它们提供简洁明了的单一答案,不会展示五花八门的广告链接,也(暂时)不会被商业利益左右。
感谢技术进步,我终于不用在”爸,这是骗人的”和”儿子,你懂什么”之间无限循环了。
类似的救赎,tk 教主的经历显然更加硬核,但我可能来不及去建立那样的信任了:
2025-02-26 00:00:00
在包含视频播放功能的 App 中,一种常见的交互是在播放器界面的左侧上下滑动调节屏幕亮度,右侧上下滑动调节音量。我们的 iOS App 里也是这样设计的,但最近在测试过程中,发现亮度调节不生效了。
代码里面调节亮度的实现是这样的:
- (void)setBrightnessUp {
if ([UIScreen mainScreen].brightness >=1) {
return;
}
[UIScreen mainScreen].brightness += 0.01;
// ...
}
- (void)setBrightnessDown {
if ([UIScreen mainScreen].brightness <=0) {
return;
}
[UIScreen mainScreen].brightness -= 0.01;
// ...
}
这个实现在较早之前是没有问题的,那我首先想到比较可能是因为系统的更新,对这个 API 做了变更。于是先查阅了 UIKit/UIScreen/brightness 的官方文档,里面只提到了 brightness 属性只在 main screen 上被支持,取值范围是 [0.0, 1.0],以及亮度调节后,直到锁屏后才会失效——即使用户在锁屏之前已经关闭了 App。并没有看到什么值得特别留意的。
然后继续看代码里的 UIScreen.mainScreen,这个属性被标记为:
API_DEPRECATED("Use a UIScreen instance found through context instead: i.e, view.window.windowScene.screen", ios(2.0, API_TO_BE_DEPRECATED), visionos(1.0, API_TO_BE_DEPRECATED))
但当前在我使用的 SDK 18.2 版本中,这个属性应仍可正常使用。
在 Google 和 StackOverflow 找了一圈,大家讨论亮度调节不生效主要集中以下方面:
也没有找到什么能匹配我的场景的解决方案。
加了一些日志,在调节亮度前后分别打印了 brightness 的值,发现它在调用 setBrightness 方法后并没有发生变化,也没有报错和告警,看起来就像是这个方法根本没有被调用一样。
也做了一些其它尝试,比如把调整亮度的代码显式调度到主线程、使用 view.window.windowScene.screen
替代 UIScreen.mainScreen
等,但都没有效果。
无奈之下,我问了 GitHub Copilot 一嘴,它的回答是这样的:
我按它的建议检查了权限,确认了不存在权限问题。
有点绝望之际,看到它提供的代码里调整亮度的粒度是 0.1,而我的代码里是 0.01,于是我尝试将粒度改为 0.1,然后奇迹发生了,亮度调节生效了。
这就有点匪夷所思了……于是我又尝试了其它的粒度值,结果如下:
我找到安装了以前老版本 App 的一个老平板(iOS 10.3.3),在上面测试了一下,发现在这个版本上,0.01 的调节粒度是可以生效的。
也就是说,在 iOS (10.3.3, 18.2) 之间靠近后者的某个版本上,[UIScreen mainScreen].brightness 的调节粒度发生了变化,由 0.01 变为了 0.05。
至此破案了,顺便吐槽一下,官方文档里对此毫无提及,实在是……略坑。
2025-02-25 00:00:00
说来惭愧,独立支撑公司的软件系统已经一年有余,多数的精力都在开发和迭代 Web 服务与 Android 端,对于 iOS App 则是一直没有更新,遇到相关的 bug 反馈也是能拖就拖——毕竟,大多数情况下找个 workaround 还是不难的。
回过头想想,可能潜意识里一直有点犯怵,觉得 iOS 开发是自己的薄弱环节,所以总想着等有时间,再多学一点相关的东西,准备得更充分、更有自信能处理好了,再去更新。可一直这样下去也不是办法,所以春节前结合一些业务需求,我决定逼自己一把,尽快把 iOS App 更新一下。
面对一个个所谓难题:
然后在这个 AI 大行其道的时代,作为尊贵的 GitHub Copilot Pro 用户,在插件的辅助下光速添加了一个新的小功能,修复了一些 bug 后,我向 App Store Connect 提交了我的第一次版本审核,本以为需要经过漫长的等待,结果……
事情出乎意料地顺利,几个小时就通过了,这玩意也有新手保护期?
满怀得意我心欢喜,于是一鼓作气把囤积已久的几个 feature 给做了,然后兴冲冲地提交了第二次版本审核,结果……
几个小时后第一次被驳回,原因是:
Guideline 3.1.1 - Business - Payments - In-App Purchase
We found in our review that your app or its metadata provides access to mechanisms other than in-app purchase for purchases or subscriptions to be used in the app, which does not comply with the App Review Guidelines. Specifically:
- Your app's binary includes the following call-to-action and/or URL that directs users to external mechanisms for purchases or subscriptions to be used in the app:
User have to contact customer service to puchase credits.
看截图我查到了这是几年前参考一个大厂 App 实现的效果,在用户余额不足时,弹出一个提示框,上面有两个按钮,一个点击后展示了客服的联系方式,一个是「取消」,点击后跳转到充值页面。
我以为这里面的主要问题点是没有明确的「去充值」入口,导致审核人员以为用户无法直接充值,必须联系客服,于是我添加了一个「去充值」的按钮,将「取消」按钮的动作改为隐藏提示框,然后再次提交审核,结果几个小时后又被驳回了,原因仍然是:
- Your app's "xxx" page includes the following call-to-action and/or URL that directs users to external mechanisms for purchases or subscriptions to be used in the app
不过发过来的消息里还有这样一段:
Bug Fix Submissions
The issues we've identified below are eligible to be resolved on your next update. If this submission includes bug fixes and you'd like to have it approved at this time, reply to this message and let us know. You do not need to resubmit your app for us to proceed.
Alternatively, if you'd like to resolve these issues now, please review the details, make the appropriate changes, and resubmit.
其实,这时候我只要回复个消息,说我这次提交包含了一些 bug 修复,希望能通过审核,下次我再修复这个问题,就妥了……
但我当时脑子里不知道咋想的,可能是觉得这么个小问题,这次解决掉算了,然后就又改了一版,将那个跳转到客服联系方式的按钮去掉,又提交了一版,这时候我以为这把稳了,就收工等消息了。
回家正刷着沙雕视频呢,弹出来消息,又被拒了,这回是什么原因呢……
Guideline 2.1 - Performance - App Completeness
Issue Description
The app exhibited one or more bugs that would negatively impact App Store users.
Bug description: unable to load "xxx"
我人都懵了,同时懊恼万分,真不应该装这个逼,就该回个消息,让审核员先通过再说。
话说回来,这个 xxx 功能已经在线上跑了几年了,最近也没改过,最后思来想去,怀疑可能是审核员当时遇上了什么网络波动之类的,导致没加载出来。
没辙,我只好在一些设备上复测了一下,保存了一些该功能正常使用的截图,然后回复给审核员,表明这个功能经我多次测试是正常的,已经上线运行了几年且最近没有修改过,希望审核员能再次确认,如果可以的话帮忙通过审核。
然后这回真的是漫长的等待,等了两天多,度过了一个忐忑的周末后,终于在周一一大早盼来了好消息。这一个版本的审核历时五天,经过了三轮被拒,总算是磕磕绊绊地通过了:
以上就是我这个 iOS 开发新手的前两次 App 审核经历,总结一下,主要有以下几点:
总的来讲,相比 Android App 需要提交到各家应用市场,然后面临不同的审核标准和结果,iOS App 的审核体验相对还是不错的,毕竟只用面对唯一的渠道和标准。
2025-01-09 00:00:00
前一阵发生过两次 Mac mini 与蓝牙鼠标断连的情况,都是通过借用别人的有线鼠标来重新连接的,终究不方便。
后来就想着,能不能通过键盘来连接蓝牙鼠标呢?摸索了一番,找到了方法,在此记录一下。
先上操作演示:
看着上面的操作,是不是感觉 so easy?但实际上,我在操作过程中,一开始就遇到一个问题——焦点无法移动到设置面板右侧的按钮上。
这时我们要先了解一个关键的设置开关,以及它对应的快捷键:
该开关默认关闭,切换开关的默认快捷键是 Ctrl + F7。
通过键盘操作,打开系统设置:
按下 Cmd + 空格,调出 Spotlight 搜索框;
输入 系统设置,回车。
通过键盘上下键,定位到 蓝牙;
操作蓝牙鼠标,使其进入配对模式;(一般是长按鼠标底部的配对键)
连续按下 Tab 键,定位到蓝牙鼠标对应设备的 连接 按钮;
注意: 这里有可能发现,按 Tab 键焦点无法移动到设置面板右侧,这时就需用到我们前面提到的设置开关了,按下 Ctrl + F7,开启键盘导航功能,再按 Tab 键就可以移动焦点了。
按下 Space 键,连接蓝牙鼠标。
大功告成!
除上了述方法,我搜索的过程中还看到有一些其它思路,比如:
可以按需尝试和使用。
2024-11-29 00:00:00
本文介绍了一种 Java 中如何用一个统一结构接收成员名称不固定的数据的方法。
最近在做企业微信的内部应用开发,遇到了一个小问题:企业微信的不同接口,返回的数据的结构不完全一样。
比如,获取部门列表接口返回的数据结构是这样的:
{
"errcode": 0,
"errmsg": "ok",
"department": [
{
"id": 2,
"name": "广州研发中心",
"name_en": "RDGZ",
"department_leader":["zhangsan","lisi"],
"parentid": 1,
"order": 10
}
]
}
而获取部门成员接口返回的数据结构是这样的:
{
"errcode": 0,
"errmsg": "ok",
"userlist": [
{
"userid": "zhangsan",
"name": "张三",
"department": [1, 2],
"open_userid": "xxxxxx"
}
]
}
就是说,不同接口的返回框架是一样的,都是 errcode + errmsg + 数据部分,但数据部分的成员名称不一样,比如上面的 department
和 userlist
。
我不知道为什么这样设计,从 Java 开发者的习惯来讲,如果由我来设计,我会尽量保持接口返回的数据结构的一致性,比如数据部分都用 data
来表示,这样在序列化、反序列化的时候可以用一个统一的泛型结构来进行。
当然这可能是企微内部的开发语言或习惯的差异,或者其它原因,这里也无法深究,只谈如何应对。
遇到这个问题后,第一反应是用 JSON 结构来接收,然后不同接口的数据部分用不同的 key 来读取。可以实现,但总觉得不够优雅。
然后想到 GitHub 上应该有不少开源的企微开发的封装库,去看看它们的实现,说不定会有更好的方案,最终果然有收获。
主要看了两个库:
前者 WxJava 知名度更高,包含的东西也更多,包含微信、企微的各种开发包的封装。它这块的实现是用我们前面提到的方法,用 JSON 结构来接收,然后不同接口的数据用不同的 key 来读取。
后者 wecom-sdk 是企微的开发包。它这块的实现是用了一个统一的泛型结构来接收数据。
以下分别截取两个库的两个部门管理相关接口的封装代码:
WxJava 版:
https://github.com/binarywang/WxJava/blob/develop/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpDepartmentServiceImpl.java
@Override
public List<WxCpDepart> list(Long id) throws WxErrorException {
String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_LIST);
if (id != null) {
url += "?id=" + id;
}
String responseContent = this.mainService.get(url, null);
JsonObject tmpJsonObject = GsonParser.parse(responseContent);
return WxCpGsonBuilder.create()
.fromJson(tmpJsonObject.get("department"),
new TypeToken<List<WxCpDepart>>() {
}.getType()
);
}
@Override
public List<WxCpDepart> simpleList(Long id) throws WxErrorException {
String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_SIMPLE_LIST);
if (id != null) {
url += "?id=" + id;
}
String responseContent = this.mainService.get(url, null);
JsonObject tmpJsonObject = GsonParser.parse(responseContent);
return WxCpGsonBuilder.create()
.fromJson(tmpJsonObject.get("department_id"),
new TypeToken<List<WxCpDepart>>() {
}.getType()
);
}
}
wecom-sdk 版:
https://github.com/NotFound403/wecom-sdk/blob/release/wecom-sdk/src/main/java/cn/felord/api/DepartmentApi.java
@GET("department/list")
GenericResponse<List<DeptInfo>> deptList(@Query("id") long departmentId) throws WeComException;
@GET("department/simplelist")
GenericResponse<List<DeptSimpleInfo>> getSimpleList(@Query("id") long departmentId) throws WeComException;
抛开 wecom-sdk 版引入了 Retrofit2 库的支持导致的代码量锐减,在返回数据的反序列化上,我也更倾向于 wecom-sdk 版的实现。
那接下来我们直接参照 wecom-sdk 里的实现方式,写一个泛型类,就可以用来接收企微的不同接口返回的数据了:
@Data
public class WxWorkResponse<T> {
@JsonProperty("errmsg")
private String errMsg;
@JsonProperty("errcode")
private Integer errCode;
@JsonAlias({
"department",
"userlist"
})
private T data;
}
这里面起到关键作用的是 Jackson 库里的 @JsonAlias
注解。它的官方文档是这样介绍的:
Annotation that can be used to define one or more alternative names for a property, accepted during deserialization as alternative to the official name. Alias information is also exposed during POJO introspection, but has no effect during serialization where primary name is always used.
Examples:
public class Info {
@JsonAlias({ "n", "Name" })
public String name;
}
NOTE: Order of alias declaration has no effect. All properties are assigned in the order they come from incoming JSON document. If same property is assigned more than once with different value, later will remain. For example, deserializing
public class Person {
@JsonAlias({ "name", "fullName" })
public String name;
}
from
{ "fullName": "Faster Jackson", "name": "Jackson" }
will have value "Jackson".
Also, can be used with enums where incoming JSON properties may not match the defined enum values. For instance, if you have an enum called Size with values SMALL, MEDIUM, and LARGE, you can use this annotation to define alternate values for each enum value. This way, the deserialization process can map the incoming JSON values to the correct enum values.
Sample implementation:
public enum Size {
@JsonAlias({ "small", "s", "S" })
SMALL,
@JsonAlias({ "medium", "m", "M" })
MEDIUM,
@JsonAlias({ "large", "l", "L" })
LARGE
}
During deserialization, any of these JSON structures will be valid and correctly mapped to the MEDIUM enum value: {"size": "m"}, {"size": "medium"}, or {"size": "M"}.
回到我们的例子,除了 department
和 userlist
之外还用到其它的 key,可以继续在 @JsonAlias
注解里添加。
这样,对不同的接口的封装,我们反序列化后统一 getData()
就可以获取到数据部分了,使用时不用再去操心数据部分的 key 是什么。
有人总问,阅读别人源码的意义是什么,这也许就可以作为一个小例子吧。
2024-10-31 00:00:00
本文介绍了一种通过自己拓展的浏览器插件,便捷地将微信公众号文章同步到阿里云开发者社区的方法。
先上效果图:
半个多月前接到 讨厌菠萝 同学的盛情邀请,入驻阿里云开发者社区,他也不时地监督我将以前的文章同步过来,奈何我懒癌晚期,一直没怎么动。
但心里其实一直记着这个事的,答应了人家,总得做点什么。
一篇一篇地手动复制粘贴实在太费劲了,也不是程序员做事的风格,于是就想着能不能找个什么工具,能帮我简化这个过程。
最后没有找到什么现成的,只好自己扩展了一个,下面就来介绍一下我的方案。
一文多发的需求其实我一直都有,以前也陆续用过 OpenWrite、ArtiPub 等工具,后来因为种种原因,只留下了 Wechatsync 这个浏览器插件。
我现在发布文章一般是先在微信公众号发布,然后再通过 Wechatsync 同步到知乎、掘金等平台。
看了下 Wechatsync 插件的原始代码仓库,目前并不支持 阿里云开发者平台。源码的最后一次更新时间停留在 2023 年 9 月,Issues 里也有人反馈了一些问题,但是作者好像没怎么回复了。
那就自己动手,丰衣足食吧,扩展一下 Wechatsync,让它支持阿里云开发者社区。
这个在 Wechatsync 的官方 API 文档里有介绍:
https://github.com/wechatsync/Wechatsync/blob/master/API.md
简而言之,如果要新增一个平台的支持,需要添加一个适配器,适配器需实现以下方法来完成工作流程:
打开阿里云开发者社区的网站 https://developer.aliyun.com/ ,登录后,打开浏览器的开发者工具,尝试进行发布文章必要的操作,查看网络请求,找到了一些接口:
有了这些,基本就够了。
作为一名前端初学者,对照着 Wechatsync 的源码里面其它的适配器,最终编写和调试完成了阿里云开发者社区的适配器。
就不把源码直接贴出来水篇幅了,有兴趣的可以去我的 GitHub 仓库查看,适配器源码直达链接:
写完适配器之后,再在 packages/web-extension/src/drivers/driver.js 文件里作少量修改,将其集成,然后就可以正常使用了。
Wechatsync 的原始作者是在 Chrome 商店上架了该插件的,只是版本不是最新。我 fork 出来做的一些修改主要自己使用,所以只是在自己的 fork 仓库里发布了最新版本,如果想要使用的话,可以用开发者模式加载:
以我将微信公众号的文章同步到阿里云开发者社区为例:
整个操作如文首的 gif 所示。
这个方案虽然不算完美,但是对我来说已经足够了,省去了很多重复的劳动,也算是一种效率提升吧。
如果你也有类似的需求,可以参考我的方案,或者自己动手扩展 Wechatsync,让它支持更多的平台。
本文所述相关源码已经提交到了我 fork 出来的 GitHub 仓库,供参考: