如何解决Merge主分支代码导致本地代码被删除?

背景

之前需求发布的时候,把代码发布到了预发布分支上,但是产品突然发现一个体验问题阻塞了发布,需要先revert掉,等改动完成后再继续发布。

现状

改动完成后,需要同步一下主分支上的代码,在本地解决冲突后再合入主分支。此时会发现Merged后将会把本地代码给删掉。

复盘定位

如图所示,其实问题的根源就是:主分支上的revert没有被撤销,导致合入开发分支的时候本地代码都被revert掉了。

解决办法

简而言之就是,撤销之前的主分支上的revert就可以让主分支上的代码合入到开发分支上又不会把本地代码删掉。相当于告诉git,主分支上这批代码不用被删掉了。具体步骤如下:

  1. 从最新的主分支上创建一个临时分支
  2. 切换到这个临时分支
  3. 在临时分支上revert(撤销)掉之前的revert提交
  4. 将临时分支合入到开发分支上,同步最新主分支代码
  5. 解决问题

如何办理香港银行卡?

背景

最近解禁了一些股票,想着等价格合适就兑现离场了。研究了一下流程才知道,美股是没办法直接兑现的,只能依靠香港做一个跳板,先把股票卖出得到美元,再转成港元出金到香港银行卡里,最后再转钱回大陆。

出入金流程

由于22年入职的缘故,当时大陆是允许本人在大陆注册富途牛牛的账户。因此,在有港卡前。我其实已经有了卷商的美股账户、卷商的港股账户,由于这个缘故也让我的出金复杂度有所降低。入职这个时间点的我,其实只要再办理一张香港银行卡就可以顺利出金了。

选择什么港卡?

不得不说小红书真的是一个好东西,有很多经验都能在上面找到。办理港卡的经验也不例外,我也是在上面了解到的香港各个银行之前的区别才做出的选择。

目前本人是在深圳工作+粤语沟通无障碍(其实业务员都会说国语,语言不会是问题)+较熟悉香港,所以亲自前往香港办事的难度不高且成本较低(签注一次15元)。综合安全性、成本、便利性等原因后选择了中银香港作为我的香港开户行。

如何预约中银香港?

1.访问“BOCHK”官网

預約開立香港賬戶 | 聯絡我們 | 中銀香港

2. 选择“中国公民身份证”和“一般账户”后,勾选同意协议,点击“继续”

3.然后就是填写一些个人资料预约时间即可

4.填写好资料点击提交就能预约成功了

吐槽

不得不说香港银行的信息化水平真的是落后大陆至少10年了,因为工作日需要工作缘故,我预约银行的时候都只能选择周六,但选择时候后总是得一个个选,然后点“选择分行”才知道当日该时段是否可以预约,效率真的是低的可怕!因此我这里可以分享一个我编写的数据分析脚本,希望可以帮大家更方便找到可以约的银行。

脚本

(function() {
    'use strict';
    const isDev = true; // 开发者模式(是否展示URL)
    const onlyShowCanBook = true; // 仅展示可以预约的银行

    function OpenTimeTable(date, time, district, count) {
        this.date = date;
        this.time = time;
        this.district = district;
        this.count = count;
    }

    const url = '/whk/form/openAccount/jsonAvailableBrsByDT.action';
    const district = [];
    const districtName = [];
    const timeTables = [];
    timeTables.push(new OpenTimeTable("日期📅", "时间段⌚️", "区🗺️", "数量⏱️"))
    Object.values($("select[name='bean.district']").children()).forEach((item) => {
        if (/^_/.test(item.value)) {
            district.push(item.value);
            districtName.push(item.label);
        }
    })

    const appDate = $("input[name='bean.appDate']")[0].value;
    const appTimeChildren = $("select[name='bean.appTime']").children();
    const appTimeLimit = appTimeChildren.length;
    const appTimeNames = [];

    Object.values($("select[name='bean.appTime']").children()).forEach((item)=>{
        if(item.localName === "option" && item.value !== ""){
            appTimeNames.push(item.label);
        }
    })

    if (appDate !== "") {
        let total = (appTimeLimit - 1) * district.length;
        for (let i = 1; i < appTimeLimit; i++) {
            const appTime = "P0" + i;
            const appTimeName = appTimeNames[i-1] || "未知"
            district.forEach((item, index) => {
                $.ajax({
                    type: "POST",
                    dataType: "json",
                    url: url,
                    data: "bean.appDate=" + appDate + "&bean.appTime=" + appTime + "&bean.district=" + item + "&bean.precondition=D",
                    success: (res) => {
                        isDev && console.info("success>>", "bean.appDate=" + appDate + "&bean.appTime=" + appTime + "&bean.district=" + item + "&bean.precondition=D");
                        if (onlyShowCanBook && res.length - 1 === 0) {
                            total--;
                            return;
                        }
                        timeTables.push(new OpenTimeTable(appDate, appTimeNames[i-1], districtName[index], res.length - 1));
                    },
                    fail: () => {
                        isDev && console.info("fail>>", "bean.appDate=" + appDate + "&bean.appTime=" + appTime + "&bean.district=" + item + "&bean.precondition=D");
                        if (onlyShowCanBook) {
                            total--;
                            return;
                        }
                        timeTables.push(new OpenTimeTable(appDate, appTimeNames[i-1], districtName[index], -1));
                    },
                });
            })
        }
        const intervalId = setInterval(() => {
            isDev && console.log(timeTables.length, total);
            // 需要将表头减掉
            if (timeTables.length - 1 === total) {
                console.table(timeTables);
                clearInterval(intervalId);
            }
        }, 3000);
    }

})();

使用指南

此处以Firefox为例,Chrome应该是类似的。

  1. 在网页的“预约日期”字段选择想要查询的日期
  2. 打开F12
  3. 将代码粘贴到“1”处
  4. 在“2”处选择代码运行的页面层级为“预约开立香港账户”
  5. 点击“3”处的“运行”按钮
  6. 等待代码执行后,会在“4”处展示之前选择的日期下,可以预约的银行位置与时间段
  7. 结合下面这张区域划分图和自己的行程安排预约就行

行动攻略

材料

  • 通行证
  • 过关小票
  • 身份证
  • 信用卡账单
  • 工卡
  • 在职证明
  • 股票授予合同(电子)
  • 10,000HKD 现金(可以找银行换汇/香港按ATM)

过关

07:30 起床

08:00 出门

09:00 到达福田口岸

09:15 到达香港

09:20 落马洲上车

10:30 到达长沙湾

10:45 到达李郑屋邨分行

开户

  1. 进门直接跟保安说明要开户
  2. 被带到人工柜台
  3. 营业员确认预约时间并且发放号码小票
  4. 营业员表示可以自己先通过BOCHK的APP进行资料预填写
  5. 填写完一直等到11:15,另外一名营业员过来接待
  6. 出示身份证、通行证和过关小票
  7. 开始问开户理由(15分钟)
    1. 表示用来作公司股票分红出金用
    2. 要求证明资金来源,出示电子授予合同
    3. 要求证明公司身份,出示工卡
    4. 要求证明居住地,出示流水账单、
  8. 拟定合同,开港元储蓄账户(已经有卷商的投资账户了,就没必要开中银的了,我这个也只是作跳板用,需求不高)
  9. 主管审核
  10. 港元账户开户完成
  11. 教授如何使用手机银行+如何使用BOC Pay(港版云闪付)
  12. 出门激活账户

激活账户

营业员明确表示需要当日存款10,000HKD激活账户。如果像我一样没有预先准备这么多的现金,可以直接到带有”银联”标志的ATM机取款,汇率跟银联的实时汇率。中银的取款机单次取款有4,000HKD的限制,所以需要插三次卡,取三次才够钱。

取完钱后,去隔壁存款机上把钱存到卡里即可激活账户,只要保证在手机银行里能看到结余有10,000HKD就行。其实这个钱也不用给营业员看,我猜测这里应该是会有风控限制,要求至少存10,000HKD才行。不过我也无所谓,优先解决了港卡问题就行。

投资账户

当日无法直接开设投资账户,找营业员了解后得知需要有特定的营业员才能开,但当日人家休假了,所以没办法直接开。后面如果有了一定流水后可以尝试在手机银行上开启,但如果触发了风控就还是需要本人到香港才能办理投资账户。这里我的需求其实没有,因为之前富途牛牛已经有投资账户了,我直接用富途牛牛的就好了。

卡片邮寄

现场是没办法将卡片改成使用快递邮寄的,只能使用平邮等一个月才能寄回大陆,我不急着用卡就还好,即使后面寄丢了,我看别人分享也可以要求银行重新寄件解决,问题不大。

补充

果不其然,银行卡平邮还是寄丢了(即使留了信箱地址,但银行只给写到XX省XX市XX路,连门牌号都不写,能寄得到就有鬼了)。最后还是花了100港币,找银行寄DHL才收到实体卡。另外补卡是需要锁定卡的,锁定之后需要肉身下香港,在ATM上办理一次业务才能解锁(插卡到ATM里查一次余额也可以),会很麻烦,尽量第一次弄的时候就让银行帮你寄快递就好了,别省那点钱了。

2024/07/21 更新:

平邮的银行卡终于寄到了我的邮箱里了,太牛逼了,去年9月份寄出的银行卡,快一年才到我的手里。中银香港不愧是你,邮政服务是真的垃圾。

总结

总共花费了1个小时才开完账户,效率实在是太低了,而且周六银行13点就关门,其实办完我这个开户后,营业员就准备下班了,想再办一家已无可能。另外办卡前的审核贼严格,跟审问犯人一样,好在对港资银行有点概念,肯定屁事贼多,带齐了所有材料,不然真的有可能白跑一趟。不过最后解决了问题就行,我也就安心了。

大漫匠 小恶魔Lilith

4月底的时候逛阿B的会员购偶然看到了这个景品手办,想着表情和体态都挺色气,价格也不贵就剁手下单了。后面忙起来其实我也忘记这回事了,周二的时候居然来短信说可以给补款了,遂直接拿下。收到货之后被惊艳到了,真的好漂亮啊,而且好大一只,199元真的是性价比拉满了。不过由于有点奔放我实在是没信心摆在工位上,只好放在公寓的电脑旁了,望着小恶魔,感觉工作起来都更有劲了。

JS 代理模式实践

背景

最近在工作中遇到了需要对Module中导出的方法统一做预处理,在特殊场景下需要增加阻断执行并提示开发者不应该在该场景中使用该方法的提示。

思考

这里一开始的想法其实是在每个方法的入口分别增加一个阻断的判断逻辑:

如果只有一两个方法其实这么做倒还好,但现在的问题是,我手上有30多个方法都要加这个阻断逻辑,手搓阻断逻辑实在是太傻逼了,这么加不知道得加到猴年马月去。

后面我又看到Module的export default,想着能不能在这里做一下文章。

研究了一下之后,其实还是可以搞的,大致思路如下:

在ES6中,javascript引入了代理函数,从而实现对象基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)[1]。通过利用其拦截的特性,我们可以借此在module上实现代理模式。在外部函数获取method的时候做一些特殊逻辑。

实现

通过该方法,我们可以优雅的在module层面对method进行统一的前置操作(比如数据上报、逻辑阻断等操作),提升开发效率的同时还能使得代码更加简洁易维护。

兼容性

通过查阅caniuse,可以说这个接口已经100%兼容可用了。另外由于ES5的特殊性,Proxy是没有完整支持的polyfill的,因此如果碰到实在不兼容的用户,就积极引导用户去升级机器吧,没必要去钻这个牛角尖。

Android外网基本上只有电视盒子还是在4.4.4,手机几乎找不到4.0的机器,就算有看着0.08%的用户占比感觉可以直接忽略了。

[日常碎碎念] 周末打卡COA

缘起🏠

师弟这个月开始在深圳实习了,刚好他也可以喝酒,终于有酒搭子,能陪我去香港打卡心心念念的COA了。

特色🌺

COA是一家主打龙舌兰作为基酒的酒吧,正如其特点,COA的名字其实就是意指收割龙舌兰所用的类似于铲子的农具,店里各处都充斥着龙舌兰的装饰,加上墨西哥风格的BGM,特别有喝酒的氛围。

奖项🏆

#1 Asia’s 50 Best Bars 2022 by 50 best bars

#1 Asia’s 50 Best Bars 2021 by 50 best bars

#7 The World’s 50 Best Bars 2021 by 50 best bars

#8 The World’s 50 Best Bars (Highest Climber) 2020 by 50 best bars

酒水🍷

这次去主要喝了两杯酒:

1. Three Salt Magarita

来到主打龙舌兰作基酒的酒吧,怎么能够不喝Magarita呢?COA比较有趣的其实是盐的部分,这里使用了三种盐:海盐、虫盐和蚯蚓盐,海盐就是经典风味,虫盐和蚯蚓盐主要的区别是有一点点的烟熏味,还挺好喝的,主打的就是一个猎奇。

2. Smacked Cucumber Salad

顾名思义,就是通过酒水在口腔中复刻了一道拍黄瓜。入口先是酸,随后是黄瓜的清香,接着是点睛之笔的麻油味与香菜味,最后才是Tequila的酒味,酸咸辣香,四种味道融合的非常棒,特别惊喜的一款特调。

零食🥔

附赠的零食是一碗品客的洋葱奶酪薯片,特别酥脆,搭配Smacked Cucumber Salad回味无穷。

服务💁

无论是Bartender还是Waiter都支持普粤英三种语言的交流,扬手即响应,不会因为你只讲普通话就给臭脸你看,秒杀外面一众香港餐厅。酒水出品飞快,即使只有2个bartender,我们的酒水在10分钟内就能给到👍。

不过最好是懂英文去,不然连最基础的特调酒单可能都会看的一头雾水。(搞不懂为啥不整一个中文备注,全英文看着着实有点费力)我去的那天外场有个应该是土耳其裔的Waiter只会说英文,这种时候如果不会英文的话就只能找其他服务员了。

价格💵

120HKD/杯+10%服务费

说实话不便宜,但在香港港岛这边其实就还好,不算很离谱,算是物有所值。

总结

如果不用排队或者第二轮就能进去(排队1小时内),我还是很愿意经常去的,毕竟真的很好喝,但超过一个小时,感觉就完全没必要了,换别的喝喝其实也不错。

如何判断一个JS方法的兼容性?

背景

由于业务的原因,我们的H5页面必须兼容一些比较老的webview版本,因此在开发中难免会遇到不支持的JS语法,因此在这里总结了一个切实可行的方法来提高自己确认接口兼容性的效率。

方法

通过caniuse.com查询方法兼容性

例子

假如需要使用Proxy方法来统一代理所有的接口,但是我不确定系统兼容性,怎么办?

1.访问caniuse.com

2.查询对应的方法名

3.查询iOS的兼容性

4.查询Android的兼容性

5.如果查询到的最低支持版本在需要的版本内则可以使用,另外在使用前最好在方法外套一层try...catch避免出现不支持的意外情况

WebAudio使用小结🧭

背景

最近在负责游戏项目的开发,不免需要接触到音频相关能力的开发。在移动端实践后才了解到,iOS下的AudioDOM并没有被完全实现,比如iOS下就无法直接通过JS控制AudioDOM的音量大小,只能控制其是否被静音,另外静音拨杆的优先级比音量键的优先级要高。如果用户通过拨杆打开了静音模式,AudioDOM默认是没办法控制音量的。

尝试解决问题

由于需要实现通用能力中规划的音频的响度控制,搜索一圈后发现可以引入WebAudio来解决问题。(https://stackoverflow.com/questions/27296391/is-there-any-possibility-to-control-html5-audio-volume-on-ios

改造前音频架构

初始化阶段的时候,通用组件会检查DOM树是否存在AudioDOM,如果不存在就会尝试创建一个新的AudioDOM并挂载到DOM树上。状态操作也是直接对DOM树上的AudioDOM操作。

改造后音频架构

WebAudio支持多种方式创建音频输入,即可以直接通过AudioBuffer获得音频输入,也可以通过createMediaElementSource从已有的AudioDOM上创建音频输入。此处我使用的是后者,通过在AudioDOM与扬声器之间创建了一个“代理层”,将音频输出通过GainNode(增益器节点)处理了一下。通过GainNode我们可以设置音频的响度,间接的实现了iOS的音量控制。

错误上报

不幸的是,测试的Testcase还不够完善,业务被发布后,错误量激增,告警拉满😂。

经过梳理后主要是以下三种问题:

  1. Failed to construct ‘AudioContext’: The number of hardware contexts provided (6) is greater than or equal to the maximum bound (6).
  2. The provided value is non-finite
  3. undefined is not a constructor (evaluating ‘new(window.AudioContext||window.webkitAudioContext)’)

1.AudioContext只能创建6个实例

Webview版本小于66中只能创建小于等于6个的AudioContext,会导致页面无法继续播放音频。(https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)

解决办法:

1. 增加全局AudioContext池,通过一个实例来管理最大音频对象数量。

2. 不使用的AudioContext需要被及时释放,避免占用音频对象资源。

2.音频节点提供的不是有限值

在某些环境中,JS的除运算计算出来的值不是一个有限值,直接赋值给WebAudio的节点会报该错误。

解决办法:

赋值前通过isFinite函数检查是否是有限值,如果不是则使用一个固定的值作为兜底即可。

3.window.AudioContext不存在

按道理来说AudioContext在执行JS代码之前就应该被挂载到window上了。但遗憾的是,iOS15之后的WKWebview会经常出现,执行JS代码的时候AudioContext仍然未被挂载到window上,导致执行构造函数的时候报错。

解决办法:

捕获错误并且定时重试,一般第一次失败的2-3秒后就能创建成功了。