注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

程序员转行做运营,降薪降得心甘情愿

自2019年末新冠疫情爆发以来,近三年的就业形势一直不太乐观,大厂裁员的消息接踵而至。身边的朋友都在感慨:现阶段能保住工作就不错了,新工作就算了。 但,就是在这样严峻的大环境下,我的前同事不三不仅跳槽还转岗,1年的转行之路,经受了各种磨难。通过小摹的热情邀请,...
继续阅读 »

自2019年末新冠疫情爆发以来,近三年的就业形势一直不太乐观,大厂裁员的消息接踵而至。身边的朋友都在感慨:现阶段能保住工作就不错了,新工作就算了。


但,就是在这样严峻的大环境下,我的前同事不三不仅跳槽还转岗,1年的转行之路,经受了各种磨难。通过小摹的热情邀请,和不三聊了聊程序员转运营过程中的经验与心得。


小摹把这份干货分享出来,希望能为每一位即将转行的伙伴提供动力支撑,也能给其他岗位的朋友新增一些不同视角的思考。


试用期差点被劝退


小摹:从事前端四年,是什么让你下定决心转行?


不三:后续有创业的打算,所以希望自己在了解产品研发的基础上,也多了解一下市场,为自己创业做准备吧。


小摹:你做的是哪方面的运营呢?这一年的感触如何?


不三:运营岗位细分很多:新媒体运营、产品运营、用户运营、活动运营、市场推广等,我所从事的是内容运营和用户运营。


公司是SaaS通信云服务提供商,对于之前从未接触过这方面工作的我而言,门槛比较高。为了能尽快熟悉产品业务,也能让我更了解用户,为后续用户运营和内容运营打基础,领导安排我前期先接触和客户相关的工作。


我试用期大部分的工作都涉及到和用户打交道,他们总会反馈给我们各种产品的需求和bug,我基本都冲在第一线安抚用户。Bug提交给开发后或许还能尽快修复,而需求反馈过去后,只能等到那句再熟悉不过的话“等排期吧”。


图片


刚做运营的前三个月,提给开发的需求大多都被驳回了,要么做出来的东西无法达到预期。那段时间,每天上班心态濒临崩溃,颇有打道回府之意。


转正之前,领导找我谈了一次话,让我醍醐灌顶:


运营身为提需求大户,你连需求都没规划好,想一出是一出,产品开发为啥会帮你做?


你之前是前端,设身处地的想,是不是非常反感产品或运营给你提莫名其妙的需求?不注重用户体验、忽略了产品的长远发展,即便当下你的KPI完成了,你有获得真正的成长,产品有迭代得更好吗?


在和领导沟通的过程中慢慢意识到,我把自己的位置摆错了,即使运营是结果驱动,但我直面用户,所以我必须要学会洞察用户的心理,重视产品的长远发展,这样才能让我有所进度。


跟领导聊完之后,我便开始调整了工作状态和节奏,明白了自己的不足,接下来就是有目标、有计划的解决问题。


回到岗位后,我梳理了公司的业务方向,写好MRD(市场需求报告),重新制定了我的运营策略,提交给了领导。


三天后,人事找到我:我通过了试用期,成功转正了。


图片


我很感谢我的领导,尽管试用期我做得很烂,但他仍然愿意给我机会,让我转正,继续工作。现在回过头看这一年,试用期阶段很痛苦,找不到工作的方向,但后来越来越熟悉了解后,也能更快上手了。


小摹:你认为一名优秀的运营要具备什么样的特质?


不三:现在的我只能说刚刚入门,我发现身边的运营大佬身上有以下特点,我希望自己能尽快向他靠拢。




  • 用户体感:所有的产品研发出来后,面向对象一定是用户,那么产品的使用体验、页面设计、活动机制、规则设定是否都能满足用户的胃口。




如果只是冲着所谓的KPI目标,而忽略了用户体验,或许你会收获万人骂的情况。


例如,随时随地朋友圈砍一刀的拼夕夕。




  • 把控热点能力:无论做什么方向的运营,都逃不了蹭热点,你可以说蹭热点low,但不可否认它会给自己和产品带来新机遇。




例如,写一篇文章蹭了热点之后,爆的几率更大;疫情刚出现时,异地办公、社区团购也随之应运而生。




  • 产品思维:互联网运营和产品经理的联系是非常紧密的,所以在推广的过程中,需要和产品部门多多碰撞。这样不仅能收获创意灵感,还能学到不少的产品思维。




在需求迭代时,应该站在更高的层次思考问题,一味给产品做加法,根本行不通。




  • 数据思维:运营以结果为导向,从数据中发现不足,从数据中发现增长点,弥补缺陷,让增长幅度更大。程序员比较有优势,可以写SQL导数据,但拿到数据只是第一步,还要懂得分析才行。




  • 抵御公关风险:例如我们在做活动时,我们要提前考虑活动的风险有哪些,如何积极应对,当有别有用心的人利用规则薅羊毛时,也应该有相应的解决方案。




图片


这段简单且干货的采访随着烧烤啤酒的上桌步入了尾声。最后不三给我说到:


一年前我调整了自己的职业方向,从前端步入运营,苦涩欢笑并存,有时看着达到目标很是激动,有时苦于KPI的折磨。一年间,我经历了人生的成长,思想也更加成熟。但我还没有达到最终目的地,现在的一切只是为了以后的创业蓄力。我不想一辈子为别人打工,也想为自己活一次。


图片


===


后记


小摹见过太多转行失败的案例,所以很为不三感到高兴,不仅仅是为他的转行成功,更多的是他坚定人生的方向,并为之做出了各种努力而高兴。


给大家分享这段采访经历,是希望大家能尽早对自己的职业生涯有所规划,有了目标后,再细分到某一阶段,这样工作起来积极性也会更高。停止摆烂,对自己负责!


人生之难,一山又一山,愿你我共赴远山。


设计1+2,摹客就够了!


作者:摹客
来源:juejin.cn/post/7158734145575714853
收起阅读 »

因为数据库与项目经理引发的一点小争执,保存留念

前言        作为刚步入社会的小同学来说,对代码有热情是很好,但是也极其嫌麻烦,明明都做完了还要被要求一遍又一遍的更改,相信大多数人都是嫌麻烦,然后就是两人之间的打情骂俏。 项目经理:你改一改嘛🤤 我:哎呀,好麻烦啊,不给你写了一个么😭 项目经理:你那...
继续阅读 »

前言


       作为刚步入社会的小同学来说,对代码有热情是很好,但是也极其嫌麻烦,明明都做完了还要被要求一遍又一遍的更改,相信大多数人都是嫌麻烦,然后就是两人之间的打情骂俏。



项目经理:你改一改嘛🤤


我:哎呀,好麻烦啊,不给你写了一个么😭


项目经理:你那个我数据库不能维护啊,快改改,乖o(^@^)o


我:😣我不我不,为啥不能维护,我不理解


项目经理:你去试试😣球球了,你去试试😭


(当然没我写的这么肉麻嘞🤣,如有雷同,纯属巧合)





       好了,数据库维护,他从前端页面进入后向页面输入肯定要调用sql,问题来了,以下这种形式sql可以是实现随意添加么(没有主键)
在这里插入图片描述
       我写python的第一反应:这有啥问题么,数据库我会个简单的增删改查,但是我感觉应该有函数可以直接往后加吧(很chun的想法,两种不同的语言怎么可能会一样),于是乎我开始了,漫漫搜索之路(因为回家连不上内网mysql,以下用Oracle代替)


使用insert函数



  • 数据库基本增加操作:insert into table_name (column1, column2, ...) VALUES (value1, value2, ...),这里直接跳过全字段添加,选取单字段添加,本以为他会如下图:


INSERT into wang.gjc_data (a1) values ('a');

在这里插入图片描述



  • 实际上如下图(哪怕是选取单字段也是默认增加一行):
    在这里插入图片描述



       我确实懵了,以前从来没有想过这件事,因为从数据库读取下来很多时候数据第一步就是先转置,感觉有点麻烦吧,因为转置完会出现很多意料之外的情况,但是人家数据库就是这么存的,现在轮到自己建数据库才发现数据库规则可太多了,而且自己上传数据也都是一次上传一行,没遇见过也就没有真正想过数据库在没有主键的情况下可以单单只改一个数据么,但是吧,我头铁啊,python能做到为啥数据库不行,我还是不信,我继续搜




  • 多条一次性插入:INSERT ALL INTO table_name (column1, column2, ...) VALUES (value1_1, value1_2, ...) into table_name(column_name1,column_name2) values (value1,value2)...select * from dual;


INSERT ALL 
INTO table_name (A1,A2) values ('a','b')
INTO table_name (B2,C1) VALUES ('c','d')
select * from dual;

       结果显而易见,肯定不是我所期望的那个场面,如下图:
在这里插入图片描述



       说实话我是真搜不着啥信息,找不到想要的答案就全试一遍,撞到南墙就回头了!所以我决定接下来从update语句下手。





使用update函数


       我想想,update好像无法新增一列,好像还没开始就结束了,但是实际页面肯定需要这个条件,那试试能不能达到自己想要的画面


       因为没有主键,所以我选择直接用update,最后结果与预料的一样,一列全部改变,图下图:


update  GJC_DATA set GJC_DATA.c2= 'c2'


在这里插入图片描述
       然后我就想到了第二范式的概念:第二范式要求在满足第一范式的基础上,非码属性必须完全依赖于候选字,也就是要消除部分依赖。
没有主键形成依赖,不满足第二范式。但是好像就算我加上一列自增主键,也无法用insert插入一个指定位置而不是一次插入一行,但是update是可以实现的,如下图(重新创建一个数据库表):


CREATE TABLE WANG.gjc_data(
id int NOT NULL,
a1 varchar(128),
a2 varchar(128),
a3 varchar(128),
a4 varchar(128),
a5 varchar(128),
b1 varchar(128),
b2 varchar(128),
c1 varchar(128),
c2 varchar(128),
c3 varchar(128),
c4 varchar(128),
c5 varchar(128),
c6 varchar(128),
c7 varchar(128),
PRIMARY KEY(id)
);
create sequence id_zeng_1
start with 1 --以1开始
increment by 1;
insert into wang.gjc_data (id,A1,b1) values(id_zeng_1.nextval,'a','d');
insert into wang.gjc_data (id,A1,b1) values(id_zeng_1.nextval,'b','e');
insert into wang.gjc_data (id,A1,b1) values(id_zeng_1.nextval,'c','f');

在这里插入图片描述


update wang.gjc_data set A1='B'  WHERE id=1;

在这里插入图片描述



       好吧,认清现实了,不过insert一次插入一行,下面直接插一行我python使用的时候早就可以用pandas清空空值,他也无法接受,可能他觉得客户看起来不好看吧,得,那凑活给他改改




  • Oracle数据库


在这里插入图片描述



  • Jupyter读取Oracle


在这里插入图片描述


总结


       到这算是结束了,总结一下,我原以为是我数据库学的不精通,做不到指定位置添加,经过这么一番探索后才发现真的没有这种操作,果然,实践才是检验真理的唯一标准,不遇上这事我还真一直有这个误区,算了,这次被自家人嘲笑就嘲笑了,那也比到时候出差去外面丢人强。



       谨以此文提醒自己,不再犯相同错误,数据库并不可以向excel那样用语句向指定位置插入指定值,更新也是需要设置主键或是一列唯一值去做一个指引;理论知识还是比较薄弱,需要持续加强。



作者:LoveAndProgram
来源:juejin.cn/post/7187287554796814393
收起阅读 »

如何快速的掌握一门编程语言

因为飞书底层是用Rust开发的,所以最近一段时间都在写Rust的代码,我对写Rust也越来越顺手,速度甚至已经比我用了很多年的c++要更快了,虽然主要原因是Rust有很多语法糖,可以加快写代码的速度。作为了一个之前完全没接触过Rust的新手,也就花了几天时间,...
继续阅读 »

因为飞书底层是用Rust开发的,所以最近一段时间都在写Rust的代码,我对写Rust也越来越顺手,速度甚至已经比我用了很多年的c++要更快了,虽然主要原因是Rust有很多语法糖,可以加快写代码的速度。作为了一个之前完全没接触过Rust的新手,也就花了几天时间,便能熟练的进行Rust项目的实战开发了。


我想到我曾经还是一位菜鸟程序员得时候,学习并且掌握一门语言要花很长的时间,并且还会到处向朋友炫耀,自己会多少种编程语言,到如今,已经完全不关注自己会多少种编程语言了(因为写过的语言太多,起码也十几种),学习一门新的编程语言所需的成本也已经很低了,一般也几天时间,就能掌握这门语言,我的关注点也从我会多少种语言,转移到这门语言在解决问题和提高效率上的实用性上。


所以我写这篇文章不是为了介绍Rust怎么学,主要是想讲讲作为一个经验丰富的程序员,如何能做到前面提到的两点:



  1. 如何用很短的时间掌握一门新的编程语言

  2. 如何基于实用性出发选择合适的编程语言


如何短时间内掌握一门新的编程语言


编程语言千千万,但是基本都要解决同样的问题,我这里列出了一些最主要的问题:



  1. 如何进行任务调度

  2. 如何处理数据

  3. 如何处理异常

  4. 如何管理内存


不管我们学习哪种编程语言,都要带着这些共通的问题去进行学习。针对这几个问题,下面我一一进行讲解。


如何进行任务调度


在任务调用上,无非就是两种方式:线程和协程。线程和协程的原理我不深入介绍,就简单讲一下,线程就是一个应用线程对应了一个系统的进程,是一对一的关系,而协程是多个协程对应了一个系统进程,是多对一关系。在使用场景,线程适合高CPU消耗任务,而协程适合高频的IO任务。当我们深入掌握了线程和协程的原理,线程安全的原理,线程协作的原理等基础知识点,那么面对任何我们没接触过的新语言时,我们只需要知道该编程语言支持哪种调度方式,比如Java支持线程,Kotlin支持协程(假协程),Rust支持线程和协程,然后再熟悉这门语言进行任务调度,加锁,等待,休眠等特性的代码要怎么写,我们便掌握了这门语言至少20%的知识点了。


如何处理数据


在数据处理上,所有的编程语言都需要提供基本的数据结构,如数组,队列,Map等等。我们需要掌握的是数据结构的原理,面对不同的数据类型特性,如何选择更合适的数据结构,而当我们学习一门新的语言时,只需要了解这些基本的数据结构对应的是哪些类即可,比如Rust中的Vec,Java中的ArrayList,其实都是动态数组这一基本的数据结构,所以在Rust学习时,我很快就能熟练的使用Vec等这门语言提供的集合容器。到这里,我们已经掌握了这门新语言40%的知识点了。


如何处理异常


异常处理是编程语言中很重要的一块知识,但是新人却很容易忽略。我们都不希望程序动不动就crash了,越是优秀的语言,对异常的处理越是完善,写出的代码crash也越少,比如用Kotlin写的程序,空指针导致的crash比java要少很多。其中Rust是我遇到的在异常处理上最为严苛的,需要手动处理每一个异常。当我们熟悉这门语言该如何处理异常时,到这里便已经掌握了这门新语言60%的知识点了。


如何管理内存


习惯了使用解释性语言的开发者可能不太关注内存的管理,当我们使用Java时,Kotlin时,并不需要我们主动去释放内存,因为虚拟机会帮我们做。但是对于其他编译型语言来说,内存的回收和释放,都只能我们自己做了,最常见的就是c++语言,内存的申请和释放都是让我们写起来觉得很麻烦的地方。当我们学一门新语言的时候,一定要熟悉这门语言是怎么管理内存,即使是Java这种不需要我们手动管理内存的语言,我们需要了解它的虚拟机是如何进行内存管理的。当我在学习Rust时,我首先关注的就是Rust需要如何管理内存,因为他是一门编译型语言,性能上不会逊色c++,我以为它依然需要自己手动申请和释放内存,结果发现Rust通过所有权的机制,使得我们不需要自己手动的申请和释放内存,这种机制立刻就让我眼前一亮,因为这是我之前从没接触过的一种新的思维。到这里,当我们掌握了这门语言是如何进行内存管理的,我们便掌握了这门语言80%的知识点了。


其他


剩下20%的知识点,包括这门语言基本类型的申明,提供的API,独有的一些特性,语法糖等等,在使用过程中,就能慢慢孰能熟能生巧了。


基于实用性出发选择合适的编程语言


我们学习或者在项目中选择一门新的语言,不能谁便拍脑袋,而是要基于实用性的考虑。我常常考虑的主要有下面这些点:



  1. 性能

  2. 简单易用

  3. 安全

  4. 跨平台

  5. 足够多的社区支持


编译型的语言在性能上是要好于解释性语言的,所以我们在客户端开发时,很多对性能要求高的逻辑都是用c++来写,而不是用Java来写。我在前面提到过飞书的底层是用Rust写的,这里的底层主要是数据层,包括db,网络请求等和数据相关的逻辑,都是用Rust完成,其中一个主要的原因就是因为Rust支持携程,在IO场景上性能会更好的,在加上其他的一些考虑,比如简单易用,跨平台等特性,Rust自然便承担了这一重任。


其他的点我就不一一展开说了。我们在选择语言的时候,都会有原因,新人可能在意简单易用;创业团队可能在意跨平台;对我来说,我对性能的要求是比较高的,因为我本身就是做性能优化这一领域的。只要我们有自己的原因和目的,而不是盲目的去选择和学习即可。


android-604356_1280.jpg


我听不少开发都说过,互联网行业技术更新迭代快,学的知识很容易就过时,然后就需要重新学习。我实际是不认可这些说法的,底层的知识更新迭代很慢的,比如计算机的原理,Linux系统的原理等等,只要我们深入掌握了,其实是可以使用很长时间的,是投入产出比很高的事情,我们认为迭代快的东西,往往都是上层的技术,比如一个框架,一个门编程语言等等。但只要掌握那些迭代慢的底层技术原理,不管上层的技术迭代多块,我们都是能很快速的进行响应和跟进的。


作者:helson赵子健
来源:juejin.cn/post/7280746697832169526
收起阅读 »

30岁的我终于如愿考上了教师 | 2023年中总结

今天,应该是我做程序员这个职业的最后一天,我放弃了20+K的工作,明天过后就要离开北京,开启我新的生活。 2023年,我终于如愿的考上了教师,最近两个月为了教师招聘的事情多次往返于北京和老家,至此整个事情终于告一段落,写篇文章分享一下这一年的考试历程,也分享...
继续阅读 »



今天,应该是我做程序员这个职业的最后一天,我放弃了20+K的工作,明天过后就要离开北京,开启我新的生活。



2023年,我终于如愿的考上了教师,最近两个月为了教师招聘的事情多次往返于北京和老家,至此整个事情终于告一段落,写篇文章分享一下这一年的考试历程,也分享给看完上一篇文章一直等待更新的jym。


书接上回,在得知即将成为爸爸后,我就开始实施了回老家的计划,最终决定考取教师编。大概在去年7月份便开始准备了教师资格证的考试,我报的科目是高中信息技术学科,每天利用下班时间学习一会。这里可以给感兴趣的jy说一下教资的考试内容,教资分为笔试和面试。笔试包括三科,综合素质(科一),教育知识与能力(科二)以及专业课(科三),对于专业课来说,大部分都是平时作为程序员可以接触到的内容,以及我们常说的八股文内容,相对于程序员面试来说知识点简单很多。但是对于科一科二来说真的很让我这个理科生头疼,要背诵的内容真的太多了,没办法硬着头皮也要学,每天早上上班早来一会背一背,晚上回去了就看看培训机构的视频。在七月底,还参加了教资认证必须的普通话考试,获得了一乙的成绩。


image.png


时间来到10月,我的女儿出生了,出生那天距离教资考试还剩下1星期,那1星期真的是忙碌、累到不想说话。在考前一周也没有什么复习的机会,就这样忙忙碌碌的去参加了考试。


考场外


11月份考试结果出来了,不出意外,果然有没过的科目,科二科三险过,科一考了69分,差一分进入及格线。就这样,我不得以继续准备科一的考试,经过对之前考试的分析,自己失分点应该主要在材料分析和作文上,这次把精力主要集中在练习作文和材料分析上。终于不负众望,在今年3月份的考试中通过科一,接下来就开始准备起了面试。


image.png


面试主要是试讲,就是模拟授课,考试时台下坐着3个老师,自己在台上讲,还有模拟提问等环节。网上有着一种说法,说教资面试是人生至尬时刻,每每回想起来都想钻地缝,自己参加之后发现的确是这样,现在想起来还是尬的抠脚。试讲主要就是多说多练,相对于笔试来说通过率还是挺高的。在准备期间,媳妇就充当我的学生,每天晚上练习练习,就这样在5月份我通过了教资面试。


image.png


6月份,在经过了体检、教资认定环节之后,便顺利的拿到了教师资格证,当时还是很开心的✌🏻。


image.png


拿到证书之后,便是等待教师招聘的过程,在此期间还参加了天津某学校的教师招聘,对于笔试科目已经忘的差不多的我排名几乎倒数。至此开始意识到必须时刻让自己的理论知识保持竞争力,因为教师招聘一般就是提前一周两周发布的,并没有太多的准备机会,只能靠平时的积累。(忽略我潦草的字迹)


image.png


时间来到7月,一个令人激动的公告发布了,老家的市区发布了教师招聘的消息,信息技术科目招聘6人,对于往年来说,这个招聘人数很多了。对我来说真的是让人兴奋的消息,得到消息后,我和媳妇当即决定,她和孩子火速赶回老家,留我一人在北京安心复习。其实这次考试我并没有抱太大的希望,因为之前给我的感觉,市区的教师招聘还是比较激烈、比较卷的。所以准备笔试时也没有太大压力,照常复习。半个月后,我赶回老家参与了这次考试,考完当时就觉得凉了,所以把媳妇孩子就接了回来,跟我一起回了北京。但是生活真是惊喜不断,一个星期后查看考试官网公告,我居然进入了面试环节,为了能够安心准备面试,又不得已将孩子再次送回了老家,现在想想这两月也真是够折腾孩子的😆。


教招面试与教资面试不同,这次面试考的是说课,相对于试讲来说,“尬”点要少一点,主要考察考生表达、授课能力。考试那天,真的是人生中最紧张的一次考试,甚至觉得高考都和它差了一个等级。在巨大的压力和紧张情绪下,我幸运的通过了面试,接下来的8月就是通过体检和政审,被正式录用啦~另外说一下我考的这次教师招聘是属于人事代理,归教育局管,老家那边已经多年不招聘编制教师了,但是据之前考过的朋友和亲戚说人事代理这样的也算挺稳定的,有懂的jy可以在评论区给大家科普一下。


以上就是这一年的考试历程,因为老家这边过了30就不能参加教师招聘,所以在这一年时间内并且也是仅有的最后一年机会能考下教资+教招,觉得自己很幸运,回想起来真的是觉得感谢自己一年的努力,如愿的完成自己的人生初步规划,觉得一切辛苦都是值得的。


说点题外话,在这期间的考试过程中,我发现信息技术学科考教师的话,相对于语数外那些科目,网上的资料是比较少的,甚至有的机构不开设信息技术科目。这一年自己也是从不懂到懂的过程一步步走过来的,如果有想考教师的jy有疑问也可以留言为你解答。


作者:一个小开发
来源:juejin.cn/post/7273025562141474852
收起阅读 »

三个月内遭遇的第二次比特币勒索

早前搭过一个wiki (可点击wiki.dashen.tech 查看),用于"团队协作与知识分享".把游客账号给一位前同事,其告知登录出错. 用我记录的账号密码登录,同样报错; 打开数据库一看,疑惑全消. To recover your lost Dat...
继续阅读 »

早前搭过一个wiki (可点击wiki.dashen.tech 查看),用于"团队协作与知识分享".把游客账号给一位前同事,其告知登录出错.



用我记录的账号密码登录,同样报错; 打开数据库一看,疑惑全消.




To recover your lost Database and avoid leaking it: Send us 0.05 Bitcoin (BTC) to our Bitcoin address 3F4hqV3BRYf9JkPasL8yUPSQ5ks3FF3tS1 and contact us by Email with your Server IP or Domain name and a Proof of Payment. Your Database is downloaded and backed up on our servers. Backups that we have right now: mm_wiki, shuang. If we dont receive your payment in the next 10 Days, we will make your database public or use them otherwise.



(按照今日比特币价格,0.05比特币折合人民币4 248.05元..)


大多时候不使用该服务器上安装的mysql,因而账号和端口皆为默认,密码较简单且常见,为在任何地方navicat也可连接,去掉了ip限制...对方写一个脚本,扫描各段ip地址,用常见的几个账号和密码去"撞库",几千几万个里面,总有一两个能得手.


被窃取备份而后删除的两个库,一个是来搭建该wiki系统,另一个是用来亲测mysql主从同步,详见此篇,价值都不大




实践告诉我们,不要用默认账号,不要用简单密码,要做ip限制。…



  • 登录服务器,登录到mysql:



mysql -u root -p





  • 修改密码:


尝试使用如下语句来修改



set password for 用户名@yourhost = password('新密码');



结果报错;查询得知是最新版本更改了语法,需用



alter user 'root'@'localhost' identified by 'yourpassword';




成功~


但在navicat里,原连接依然有效,而输入最新的密码,反倒是失败



打码部分为本机ip


在服务器执行


-- 查询所有用户


select user from mysql.user;


再执行


select host,user,authentication_string from mysql.user;



user及其后的host组合在一起,才构成一个唯一标识;故而在user表中,可以存在同名的root


使用


alter user 'root'@'%' identified by 'xxxxxx';

注意主机此处应为%


再使用


select host,user,authentication_string from mysql.user;

发现 "root@%" 对应的authentication_string已发生改变;


在navicat中旧密码已失效,需用最新密码才可登录


参考:


mysql 5.7 修改用户密码




关于修改账号,可参考此




这不是第一次遭遇"比特币勒索",在四月份,收到了这么一封邮件:



后来证明这是唬人的假消息,但还是让我学小扎,把Mac的摄像头覆盖了起来..


作者:fliter
来源:juejin.cn/post/7282666367239995392
收起阅读 »

组件阅后即焚?挂载即卸载!看完你就理解了

web
前言 上家公司有个需求是批量导出学生的二维码,我一想这简单啊,不就是先批量获取学生数据,然后根据QRcode生成二维码,然后在用html2canvas导出成图片嘛。 由于公司工具库有现成的生成压缩包方法,我只需要获得对应的图片blob就可以了,非常的easy啊...
继续阅读 »

前言


上家公司有个需求是批量导出学生的二维码,我一想这简单啊,不就是先批量获取学生数据,然后根据QRcode生成二维码,然后在用html2canvas导出成图片嘛。
由于公司工具库有现成的生成压缩包方法,我只需要获得对应的图片blob就可以了,非常的easy啊。


开始动手


思路没啥问题,但第一步就犯了难,用过react框架或者其他MVVM框架的都知道,这种类型的框架都是数据驱动视图,也就是说一般情况下,必须先获得数据,然后根据数据才能得到视图。


但是问题是,html2canvas也是必须需要获取真实dom的快照然后转换成canvas对象。


听着好像不冲突,诶,我先获取数据,然后渲染出视图,在依次通过html2canvas来生成图片不就完事了嘛!但是想归想,却不能这么做。


原因主要有两个,一个原因呢是交互逻辑上就行不太通,也不友好。你不能“啪”点一下导出按钮,然后获取数据之后再去等所有数据渲染出对应组件之后,再去延迟处理导出逻辑。(耗时太长)


另一个原因呢,主要是跟html2canvas这个工具库有关系了,它的原理简单来说呢,就是复制你期望获取截图的那个dom的渲染树,然后根据这个渲染树在当前页面生成一个你看不见的canvas dom对象来。那么问题来了,因为是批量下载,所以肯定会有大量的数据,那么如果不做处理,就会有大量的canvas对象存在当前页面。


canvas标签是会占用内存的,那么当同时存在过多的canvas时,就会出现一个问题,页面卡顿甚至崩溃。所以,这是第二个原因。


那么这篇文章主要是解决第一个原因所带来的问题的。


编程!启动!


第一步


那么先简单的随便生成一个组件好了,因为是公司源码嘛,大家懂的都懂。


interface IProps {
qrCode: string
studentName: string
className: string
}

const SaveQRCode = (props: IProps) => {
const divRef = React.useRef<HTMLDivElement>(null)
// 具体怎么渲染看你们需求了
return (
<div ref={divRef}>XXXXXX</div>
)
}

看到代码,用过html2canvas的小伙伴应该知道ref是干嘛用的了,html2canvas()这个方法的参数是HTMLElement,传统一点的办法呢,可以通过document.getXXXXX这个方法来获取真实的dom元素。那么Ref就是替代前者的,它可以直接通过react框架获取真实的dom元素。


第二步


那么最简单的组件我们已经写好了,接下来就是如何动态的挂载这个组件,并且在挂载完之后就立刻卸载它。


那么先来理一下思路:
1、动态地挂载这个组件,且不能被用户肉眼观察到
2、挂载动作执行完立刻执行html2canvas获取canvas对象
3、通过canvas对象转换成blob对象并返回,或者直接通过回调函数返回canvas对象
4、组件卸载,清空dom


那么根据上面几点,可以得出:从外部获取的肯定是有组件这个东西,而挂载的位置则有要求,但并不一定需要从外部获取。


为了不被样式影响,我们直接在body标签下,再挂载一个div标签,来进行组件的动态渲染和卸载,同时也避免了影响之前dom树的结构。


思路就说到这了,接下来直接抛出代码:


const AsyncMountComponent = (
getElement: (onUnmount: () => void) => ReactNode,
container: HTMLElement,
) => {
const root = createRoot(container)
const element = getElement(() => {
root.unmount()
container.remove()
})
root.render(<Suspense fallback={null}>{element}</Suspense>)
}

这里我因为想做的更加通用一点,所以把根节点让外部进行处理,如果希望更加业务一点,比如当前这个场景必然不会让用户可见,可以直接改成


const AsyncMountComponent = (getElement: (onUnmount: () => void) => ReactNode) => {
const div = document.createElement('div')
div.style.position = 'absolute'
div.style.left = '2000px'
document.body.appendChild(div)
const root = createRoot(div)
const element = getElement(() => {
root.unmount()
container.remove()
})
root.render(<Suspense fallback={null}>{element}</Suspense>)
}

这里的隐藏方式看个人喜好,无所谓。但有一点要注意的是,一定要可见,不然的话html2canvas生成不了图片,这里是最简单粗暴的方式,直接偏移left


第三步


那么地基打好了,我们该怎么用这两个东西呢


interface IProps {
qrCode: string
studentName: string
className: string
// 这里自然就是获取blob和canvas对象的地方了
onConfirm?: (data: { canvas: HTMLCanvasElement, blob: Blob }) => void
// 这里是卸载的地方,由外部决定何时卸载节点,更加自由
onUnmount?: () => void
}

const SaveQRCode = (props: IProps) => {
const divRef = React.useRef<HTMLDivElement>(null)
useEffect(() => {
if (divRef.current && props.onConfirm) {
html2canvas(divRef.current).then((canvas) => {
canvas.toBlob((blob) => {
props.onConfirm!({canvas, blob: blob!})
props.onUnmount!()
})
})
}
}, [])
// 具体怎么渲染看你们需求了
return (
<div ref={divRef}>XXXXXX</div>
)
}

首先我们对组件进行修改,因为我的方案是第一种,没有太业务向,所以说一些业务逻辑必然是要到组件层面去处理的,所以添加两个参数,一个获取blobcanvas对象,另一个用来卸载节点。


至于useEffect就很容易理解了,挂载后用html2canvas处理组件顶层div获取截图,然后返回数据,并卸载节点。


组件改造完毕了,那我们接下来把这两个组合一下


const getQRCodeBlobCanvas = async (props: IProps): Promise<{
canvas: HTMLCanvasElement, blob: Blob
}> => {
return new Promise((resolve) => {
const div = document.createElement('div')
div.style.position = 'absolute'
div.style.left = '2000px'
document.body.appendChild(div)
asyncMountComponent(
(dispose) => (<SaveQRCode {...props} onConfirm={resolve} onUnmount={dispose}/>),
div
)
})
}

那么一个简单的动态阅后即焚组件就完成了,且可以直接通过方法的形式使用,完美适配批量导出功能,当然也包括单个导出,至于批量导出的细节我就不写了,非常的简单。


升级V2


我只提供了最通用一种方式来做这么个阅后即焚组件,之后我闲着无聊,又把它做了一次业务向升级,获得了V2版本


这个版本呢,你只需要传入一个组件进去,且不用关心何时卸载,它是最真实的阅后即焚。至于数据,会通过Promise的方式返回给用户。


const Wrapper = ({callback, children}: {  
callback: (data: { blob: Blob,canvas: HTMLCanvasElement }) => void,
children: ReactNode
}
) => {
const divRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (divRef.current) {
html2canvas(divRef.current).then((canvas) => {
canvas.toBlob((blob) => {
callback({canvas, blob: blob!})
})
})
}
}, [])
return <div ref={divRef}>
{children}
</div>

}

const getComponentSnapshotBlobCanvas = (getElement: () => ReactNode): Promise<{canvas:HTMLCanvasElement, blob: Blob}> => {
return new Promise((resolve) => {
const div = document.createElement('div')
div.style.position = 'absolute'
div.style.left = '2000px'
document.body.appendChild(div)
const root = createRoot(div)
root.render((
<Wrapper
callback={(values) =>
{
root.unmount()
div.remove()
resolve(values)
}}
>
{getElement()}
</Wrapper>

))
})
}

其实也没啥特别的,无非就是把业务层公共的东西封装进了方法里,思路还是上面那个思路。


那么这篇博客就到这里了,感谢阅读!


作者:寒拾Ciao
来源:juejin.cn/post/7278512641781334051
收起阅读 »

我的前端故事之终入BAT

前言 这个周末终于能抽出时间写写了,看到有些朋友在催更还是挺开心的,谢谢大伙。这一篇是整个系列的最后一篇,将会写到现在的时间线,今后应该会根据发生的事,较长时间才更新一次。 如果有细心的朋友看了我之前写的四篇,会发现这一篇我不再以菜鸟前端为题目了,如文章标题所...
继续阅读 »

前言


这个周末终于能抽出时间写写了,看到有些朋友在催更还是挺开心的,谢谢大伙。这一篇是整个系列的最后一篇,将会写到现在的时间线,今后应该会根据发生的事,较长时间才更新一次。


如果有细心的朋友看了我之前写的四篇,会发现这一篇我不再以菜鸟前端为题目了,如文章标题所见,我成功入职BAT之一,虽然远不能称为技术大佬,
但摘掉菜鸟的帽子,应该还是可以滴。


入职金融公司


回到当时的时间线,这家金融公司规模很大,就叫简单粗暴的叫它大金融吧,万人以上员工,这也是我选择的原因之一。另外虽然这是我当时最高的offer,但回想起来面试难度却比较一般,算法只是问了思路没有要求手写,似乎搞金融的技术都比较保守(或许是我的偏见)。


去入职之前还是比较紧张的,因为这跟我之前经历的公司规模相差太大了,完全不是一个级别。我几乎没有前端团队协作的经验,之前的两年虽然写了不少代码,但只能说是闭门造车。那时的我不了解前端工程化、甚至没有团队git的使用经验,一看到代码冲突就手忙脚乱好像犯了大错。


入职当天来到新公司楼下,位于南山核心地段的高端写字楼,物业的小姐姐小哥哥都是俊男美女,令我开了眼界。hr给我签了合同后,我见到了我未来的直系领导湖哥,其实我一度很担忧遇到一个不好相处的领导,毕竟之前遇到的小张总之类的都算是非常规的老板。幸运的是我多虑了,我又遇到了一个相当不错的leader。


湖哥是80后,本身是做客户端的,组里的老人也几乎都是客户端的,只是我们的业务是新成立的团队所以整个大前端由湖哥管理,所以实际上湖哥在web开发并没有太多的经验,很多时候我可以自己发挥。


我是这个业务web端的第一个新人,所以湖哥直接带我,在我入职半个月后部门又入职了四五个前端,整个大前端团队十余人,已经算是不错的规模。与此同时,整个部门的研发人员也由一开始的二十多人逐渐扩充到七十多人,囊括算法、服务端、前端、测试。其中也有不少名校毕业、大厂经历的大佬,甚至算法还有博士。搞得我怀疑我是怎么进来的,后期甚至招聘只要211以上,还好我进来得早。


技术进展&团队角色


大金融的技术没有我预想的那么高要求,项目还算属于常规的类型,也有算法标注平台,移动端海报编辑器等不错的项目。技术深度对于当时的我已然足够,我迅速熟悉了项目,几乎可以用无痛上手来形容。


但我仍学到了不少现代前端的知识,比如canvas动画、工程化、持续集成等方面,也了解到原来一个项目可以有那么多服务器、那么多环境。也学到了不少无用的知识,就是那些什么业务条线、触达、拉通之类的互联网黑话。


之前有提到我是第一个入职的新人,所以我天然有一些团队内优势,比如会担任新同事导师,
更加熟悉团队内项目,再加上我经历特殊,脑子比较灵活不只是死学钻技术,做人做事情商方面还不错,所以后期算是web侧的核心,用不太好的词形容就是湖哥的嫡系......


而且湖哥本身不熟悉web开发,有次上面交代了一个需要快速上线内测项目,希望不要服务端参与让我们前端自己解决,我主动跟湖哥申请接过了任务,因为我一直很有兴趣学学nodejs,有带薪实战的机会一定要抓住,即使可能会遇到很多坑。


我还记得我当时提出的时候湖哥脸上喜悦的表情,因为这事让他很头痛。


后来我独自加班加点一周自学node搞定了前后端,成功跑通了业务流程,或许在湖哥心里又给我加了一分,从那之后web端的任务基本都是我在分配,他基本不再插手交给我了,
回顾当时虽然没有title,实际上我已经算是web侧虚线leader。


更进一步的想法


大金融的日子就这么在需求与开发中一天天过去,熟悉技术和项目后,并没有太大的波澜。平时很少加班,忙的时候一个月加几天,不忙有空我就学学新技术,后来觉得php实在非主流了,还学了python(还是不喜欢java~)用在了公司项目了,扩充了技术栈。


不过也是在大金融我才感受到双休的幸福,周末有更多的时间陪对象,不用担心醒来又要上班。我第一次有了公积金,有了通讯补贴之类的杂七杂八的福利,也知道了原来年假这个东西是真实存在的。


也才意识到之前单休、随意加班的日子并不正常,只是我没介意而已。


慢慢的,我想要升职加薪,我不再满足18k,现在想来野心其实有点大,因为入职还不到一年而已。可也是在蠢蠢欲动的这个过程中,我发现大金融的阶级相当固化,leader几乎只有更换,没有晋升,而且人员流动极大,这是我之前没有体验过的,新业务并没有盈利,但是之前的疯狂扩张却埋下了隐患,在不到一年里总监、hr、架构师甚至大老板都都有离职,我甚至打听到今年的年终奖要延迟到明年六月才发,我开始意识到不对劲,要是年终不发会影响到薪资流水,我萌生了要跳槽的想法,而且是要年前跳出去。


还有一个原因是当时正值互联网顶点时期,风头正盛,比18年有过之无不及,有大把的跳槽机会,脉脉上那些大厂应届生晒出的offer薪资多次上了热搜,令我羡慕至极。


另一方面,咱们这种平民程序员,谁又没有一个大厂梦,虽然我学历很差,但是还是想试试。
万一成了呢?


全力准备面试


跟女友说了想法后她也非常支持,当然这是预期之内的,谁不喜欢自己另一半上进。也给父母说了我一定要在半年内达到30万年薪,算是给自己的一份承诺和压力,意外的是我爸妈都一致
表示不希望我太累,要生活和身体为重,他们认为我现在已经很不错了。那一辈人能有这种想法真是不容易,很感谢他们第一时间想到的是我的身体,而不是赚多少钱。其实我内心的想法也没那么极端,我知道以我的学历和经历,进大厂的可能微乎其微,我想的是至少要进个中厂,至少进入一个大家都知道的、有名气的公司,像携程、贝壳、唯品会、喜马拉雅、迅雷等,我把想去的公司列了一个目标名单,打算做好十足的准备后都去试试。


为了激励自己,我很沙雕的在拼多多买了一个条幅挂在家里墙上,想偷懒的时候就看看。


现在想来非常中二,女友看着家里的横幅一度无语,说像是进了传销窝点。


那段时间我加足了马力准备面试,公司的需求我通常只用排期的一半时间就可以做完,剩余的时间就用来准备面试,晚上回到家再学习到十点多。具体的过程倒不必刻画得多么艰辛,因为心里对目标的渴望,使我的学习过程充满了动力。总之经过有目标、有规划的准备后,从算法到源码,基本都有底。


大约持续两个月这样的日子后,我迎来了第一次面试。


再次挑战大厂


其实这次面试打乱了我的计划,因为学习计划还有20%左右没有完成,而且我本来是想先找小厂练练手找找感觉,再去试试心仪的公司。但是hr告诉我这个hc近期就会关闭,还不如现在就试试,等我准备好了可能都招满了。简单思考了下我果断决定投递简历,因为这个岗位我非常钟意,可遇不可求。


这是腾讯PCG的web开发岗,所属于一个绝大多数人都使用或者安装过的国民级应用。


为一个从小就使用的app写代码是难以想象的事,实在是遥不可及,带着很可能挂简历的预期投递给了hr。


第二天我就收到了面试邀请,看来我的简历写得还算不赖,hr也说他把简历发群里很快就被一个组挑走了。后来我了解到这个岗位对于我来说是比较偏高的职级,当时的腾讯已经几乎不会设置低职级的社招岗位了,包括现在也是如此。


了解到后我的压力又大了一分,我没有跟女友说要面试腾讯,因为我怕她预期太高后面很可能会失望,虽然她从不在事业上要求我什么。


第一面的时间就约在了周六,一下就感受到了可能会很卷,不然不会在周六面试,可是当时的我就算007也得试试,我起点太低,进大厂的渴望早就超越一切了。


带着紧张的心情迎来了一面,面试官是一个语气非常礼貌的同龄人,全程面试都是在跟我讨论问题一样的气氛,但是态度好归好,难度还是挺高的。


大厂面试特别之处在于,大厂并不在意候选人在框架上或者具体某项技术的经验,不会要求你一定在某种技术有大量经验,与小公司完全不一样。


面试范围非常全面,从计算机网络、浏览器到源码、性能都有涉及,这些都答得不错,算法没有太难用不太高效的解法答出来,但是涉及到跨端的底层、node中间件的问题我回答得并不好,因为那时候几乎没有这方面实战经验。面完我就感觉凉了,虽然早有预期但是还是很失落的,又一次机会被我错过。


虽然心里已经放弃,但我还是积极的跟hr反馈了情况,接着就是等待着"死亡宣判"的消息。


第二天带着紧张的心情点开了hr的未读消息:


一面通过!准备二面!


看到消息的我兴奋得手舞足蹈,长舒口气。现在看来还挺搞笑的,只是一面而已,可对于那时候的我是意义非凡,那是第一次通过了大厂的考验,即使只是一轮。


二面没有我想象的那么难,比第一面发挥更好,如果按照第一面的标准我应该没问题,果不其然,二面也顺利通过。后来听hr反馈,一面给了我相当高的评价,实属意外,看来自己的感受并不一定准确。


通过二面之后,我开始有了一些幻想,幻想进入腾讯的那一天,但也知道很可能也只是幻想。
三面是最难的一面,面委会的一轮。应该是出于要定级的原因,我强烈感受到了面试官想要逼近我的技术极限,虽然没有写算法但整个过程非常压迫,提了很多极其深入和刁钻的问题,结束后我知道这次绝对完了,比一面糟糕多了。


可是第二天却收到了hr通过的消息,后来才知道面委一般较少挂人,面试也是会故意提高难度,主要是给用人部门做职级参考。


再后来就是总监面、GM面、Hr面,一共经历了整整六面,加上中间还有一些小波折,接近两个月才面完,鹅厂流程长真是名不虚传。
等待offer的那段时间真是患得患失,怕没hc,怕学历太差被卡,又怕后面背调不过,总之就是心神不宁,兴奋又惶恐。


终入鹅厂


经过漫长的等待,在一个工作日,我终于收到了offer,本来想了很多形容当时心情的词,此时不知道如何去描述。但我也相信经过之前的描述和铺垫,足够把我当时的心情交给大家去想象。


当天我就提了离职,面对湖哥的失落我感觉自己真不是个人,可是这可是腾讯啊,对不起了湖哥。


晚上跟女友一起去吃了最爱吃的那家餐厅,庆祝一下,我记得很清楚,本以为那顿饭会是吃得最美味的一顿饭,却没想到是最没味道的一顿饭,因为非常兴奋已经没有精力去品尝菜的味道。跟爸妈分享后,我也感受到了他们对我成长的欣慰,毕竟我上学的时候实在太贪玩了,现在能够一直奋斗向上算是出乎意料,我的学历能进腾讯算是一个小奇迹。


说起来汗颜,我已经太久太久没能让他们为我骄傲过了。


再后面,腾讯足足找了六个前同事领导对我进行了背调,顺利通过后,我拿到了象征入职最后一步的鹅厂红围脖,腾讯员工应该都知道我说的是什么。紧接着就是拍工牌照上传,在线入职签合同,英文名是真难取,在我还没入职,就收到短信提醒我电脑等办公设备已经到了工位,新款的macbookPro,4k显示器。人家都说进大厂最开心的就是入职和离职的时候,至少前一半说对了,那段时间我真的好开心。


至此,尘埃落定,我要进大厂了,而且是top级的大厂,小时候打游戏时天天骂的腾讯。


还是说一下待遇,透露职级不太合适,类似于阿里P6,这个职级的薪资水平大家网上也能很好查到,就不说具体数字了,对于本科三年经验的我已经相当满意了,而且还有班车食堂顶格公积金等福利。


总结这几年


其实写到这里,前几年的成长主要历程就已经结束了,后面虽然也有一些小进步,但没有质的变化,不足以另立篇幅来写,待我积累积累再继续。


还有就是说腾讯是养老院的,我只能说快卷死我了,至少我所在部门是这样,增长难的背景下实在是好累。


结尾总结一下自己的历程吧,回顾下自己做对了那些事,做错了哪些事,又收获了什么。


先说说起做对的选择,回想起来实在太多,或许是因为我不错的判断能力,也可以说是我有很多的好运气。



1、没有贪图公司名气,选择某软外包去实习,无法留用。


2、初入社会遇到难题没有放弃,抓住实习机会恶补了技术。


3、实习结束果断离开小外包,来到深圳,给了自己更多可能性。


4、脱离舒适区,离开小美妆,如果当时选择躺平应该一辈子都进不了大厂了。


5、没有去宝能,不然现在可能在举横幅讨薪。


6、在大金融持续进步学习,成为核心,走上正常职业道路,接触互联网开发。


7、在察觉到大金融异常后,放弃组内积累忍痛离开(半年后大金融大裁员,年终奖没发)。


8、遇到好岗位,即使面试没有准备好也果断投简历。


9、跳槽正好抓住互联网巅峰末尾,趁着大裁员未开始准备跳槽,如果现在去面试绝无可能拿到offer。



还有一个很重要的运气因素,我一路遇到的直系领导都很好,实属难得。


再说说我认为自己做错的事,现在看来不算多:



1、专业课应该好好学,浪费四年时光,不然不至于找工作起点那么低,爬升那么艰难。


2、应该早点离开小美妆,进入更主流、更互联网的公司,或许可以早一点进入大厂积累更多,现在互联网红利不再,只尝到了一点点甜头。



从上大学开始回顾,这两点是我比较遗憾的事,当然回头以上帝视角来看,这些都是马后炮了,但持续复盘总是没错的。


最后,再聊一聊自己这几年的收获和感触吧


有些大佬看了全文可能会觉得我之前有些地方的描述是不是夸大了,不过就是进个腾讯而已,不至于写得好像功成名就一样。可就像我总说的一样,我的起点很低,985/211的朋友们可能一毕业就进了大厂,挑选着大厂的offer。
可是环境造就人,我从山村里走出来,个位数的年龄就要天没亮打着手电筒,经过泥泞的山路走上几公里才能到学校。小时候一直在班上名列前茅,后来考到了xx第一中学这样的市重点中学,可我们班高考连一个考上双非一本的同学都没有,如果从小到大都是所在环境的前10%,出生在大山里和出生在一线城市的区别就是垃圾本科和985的区别。


工作前我以为我赚了钱一定要好好撒欢一下,把工资都用来买之前想买而买不起的东西犒劳下自己,但现实是毕业后就不敢不存钱,从来没有体会过网上那些人月光的感觉。


现实里我还是很少怨天尤人的,因为一路走来我运气不错,回忆起来小时候也并不觉得苦,而是快乐更多。只是此时此景我想说一下背景,来解释为何我会对一些并不高大上的事物有那么多感触。


我对现状已然满足,虽然有时也会做做梦,期待年薪百万的那一天,但心里其实不敢再奢求更多;有时也会有一些小骄傲,我好像还是我们学校第一个进入bat的毕业生,虽然只是成为了一线城市一个的打工仔,不过也算优秀的打工仔嘛~


到这里菜鸟的故事结束了,我的故事还在继续,或许明天升职加薪,也可能是裁员滚蛋,且等我经历后再继续分享。


PS:其实写这么多我的本意主要还是留给将来的自己看,回顾自己的时光。这系列文章的阅读量加起来也只是寥寥,大约几千吧,感谢这几千位朋友的关注。


最后祝各位:



都能做出对的选择,遇到对的人,抓住好的机遇,活出好的人生,以健壮的心态,扛住生活的压测。



无论你是像我当初一样菜鸟还是功成名就的大佬,共勉!


作者:鹅猴
链接:https://juejin.cn/post/7270359123535380538
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS之WebViewJavascriptBridge浅析

iOS
前言 H5页面具有跨平台、开发容易、上线不需要跟随App的版本等优点,但H5页面也有体验不如native好、没有native稳定等问题。所以目前大部分App都是使用Hybrid混合开发的。 当然有了H5页面就少不了H5与native交互,交互就会用到bridg...
继续阅读 »

前言


H5页面具有跨平台、开发容易、上线不需要跟随App的版本等优点,但H5页面也有体验不如native好、没有native稳定等问题。所以目前大部分App都是使用Hybrid混合开发的。


当然有了H5页面就少不了H5与native交互,交互就会用到bridge的能力了。WebViewJavascriptBridge是一个native与JS进行消息互通的第三方库,本章会简单解析一下WebViewJavascriptBridge的源码和实现原理。


通讯原理


JavaScriptCore


JavaScriptCore作为iOS的JS引擎为原生编程语言OC、Swift 提供调用 JS 程序的动态能力,还能为 JS 提供原生能力来弥补前端所缺能力。
iOS中与JS通讯使用的是JavaScriptCore库,正是因为JavaScriptCore这种起到的桥梁作用,所以也出现了很多使用JavaScriptCore开发App的框架,比如RN、Weex、小程序、Webview Hybrid等框架。
如图:




当然JS引擎不光有苹果的JavaScriptCore,谷歌有V8引擎、Mozilla有SpiderMoney


JavaScriptCore本章只简单介绍,后面主要解析WebViewJavascriptBridge。因为uiwebview已经不再使用了,所以后面提到的webview都是wkwebview,demo也是以wkwebview进行解析。


源码解析


代码结构


除了引擎层外,还需要native、h5和WebViewJavascriptBridge三层才能完成一整个信息通路。WebViewJavascriptBridge就是中间那个负责通信的SDK。


WebViewJavascriptBridge的核心类主要包含几个:


  • WebViewJavascriptBridge_JS:是一个JS的字符串,作用是JS环境的Bridge初始化和处理。负责接收native发给JS的消息,并且把JS环境的消息发送给native。
  • WKWebViewJavascriptBridge/WebViewJavascriptBridge:主要负责WKWebView和UIWebView相关环境的处理,并且把native环境的消息发送给JS环境。
  • WebViewJavascriptBridgeBase:主要实现了native环境的Bridge初始化和处理。



初始化


WebViewJavascriptBridge是如何完成初始化的呢,首先要有webview容器,所以要对webview容器进行初始化,设置代理,初始化WebViewJavascriptBridge对象,加载URL。

    WKWebView* webView = [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.view.bounds];
webView.navigationDelegate = self;
[self.view addSubview:webView];
// 开启打印
[WebViewJavascriptBridge enableLogging];
// 创建bridge对象
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
// 设置代理
[_bridge setWebViewDelegate:self];

这里加载的就是JSBridgeDemoApp这个本地的html文件。

    NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"JSBridgeDemoApp" ofType:@"html"];
NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
NSURL *baseURL = [NSURL fileURLWithPath:htmlPath];
[webView loadHTMLString:appHtml baseURL:baseURL];

再看一下JSBridgeDemoApp这个html文件。

function setupWebViewJavascriptBridge(callback) {
// 第一次调用这个方法的时候,为false
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
// 第一次调用的时候,为false
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
// 把callback对象赋值给对象
    window.WVJBCallbacks = [callback];
// 加载WebViewJavascriptBridge_JS中的代码
// 相当于实现了一个到https://__bridge_loaded__的跳转
var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'https://__bridge_loaded__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
    }

// 驱动所有hander的初始化
setupWebViewJavascriptBridge(function(bridge) {
...
}

在JSBridgeDemoApp的script标签下,声明了一个名为setupWebViewJavascriptBridge的方法,在加载html后直接进行了调用。
setupWebViewJavascriptBridge方法中最核心的代码是:



 创建一个iframe标签,然后加载了链接为 https://bridge_loaded 的内容。相当于在当前页面内容实现了一个到 https://bridge_loaded 的内部跳转。
ps:iframe标签用于在网页内显示网页,也使用iframe作为链接的目标。


html文件内部实现了这个跳转后native端是如何监听的呢,在webview的代理里有一个方法:decidePolicyForNavigationAction
这个代理方法的作用是只要有webview跳转,就会调用到这个方法。代码如下:

// 只要webview有跳转,就会调用webview的这个代理方法
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) { return; }
NSURL *url = navigationAction.request.URL;
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;

// 如果是WebViewJavascriptBridge发送或者接收消息,则特殊处理。否则按照正常流程处理
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
// 是否是 https://__bridge_loaded__ 这种初始化加载消息
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
// https://__wvjb_queue_message__
// 处理WEB发过来的消息
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
return;
}

// webview的正常代理执行流程
...

从上面的代码中可以看到,如果监听的webview跳转不是WebViewJavascriptBridge发送或者接收消息就正常执行流程,如果是WebViewJavascriptBridge发送或者接收消息则对此拦截不跳转,并且针对消息进行处理。
当消息url是https://bridge_loaded 的时候,会去注入WebViewJavascriptBridge_js到JS中:

// 将WebViewJavascriptBrige_JS中的方法注入到webview中并且执行
- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
// 把javascript代码注入webview中执行
[self _evaluateJavascript:js];
// javascript环境初始化完成以后,如果有startupMessageQueue消息,则立即发送消息
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}

[self _evaluateJavascript:js];就是执行webview中的evaluateJavaScript:方法。把JS写入webview。所以执行完此处代码JS当中就有bridge这个对象了。初始化完成。


总结:在加载h5页面后会调用setupWebViewJavascriptBridge方法,该方法内创建了一个iframe加载内容为 https://bridge_loaded ,该消息被decidePolicyForNavigationAction监听到,然后执行injectJavascriptFile去读取WebViewJavascriptBridge_js将WebViewJavascriptBridge对象注入到当前h5中。


WebViewJavascriptBridge 对象


整个WebViewJavascriptBridge_js文件其实就是一个字符串形式的js代码,里面包含WebViewJavascriptBridge和相关bridge调用的方法。

// 初始化Bridge对象,OC可以通过WebViewJavascriptBridge来调用JS里面的各种方法
window.WebViewJavascriptBridge = {
registerHandler: registerHandler, // JS中注册方法
callHandler: callHandler, // JS中调用OC的方法
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue, // 把消息转换成JSON串
_handleMessageFromObjC: _handleMessageFromObjC // OC调用JS的入口方法
};

WebViewJavascriptBridge对象里核心的方法有:


  • registerHandler:JS中注册方法
  • callHandler: JS中调用native的方法
  • _fetchQueue: 把消息转换成JSON字符串
  • _handleMessageFromObjC:native调用JS的入口方法

当初始化完成后,WebViewJavascriptBridge对象和对象里的方法就已经存在并且可用了。


JS和native是如何相互传递消息的呢?从上面的代码中可以看到如果JS想要发送消息给native就会调用callHandler方法;如果native想要调用JS方法那JS侧就必须先注册一个registerHandler方法。


相对应的我们看一下native侧是如何与JS传递消息的,其实接口标准是一致的,native调JS的方法使用callHandler方法:

id data = @{ @"dataFromOC": @"aaaa!" };
[_bridge callHandler:@"OCToJSHandler" data:data responseCallback:^(id response) {
NSLog(@"JS回调的数据是:%@", response);
}];

JS调native方法在native侧就必须先注册一个registerHandler方法:

    // 注册事件(h5调App)
[_bridge registerHandler:@"JSTOOCCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"JSTOOCCallback called: %@", data);
responseCallback(@"Response from JSTOOCCallback");
}];

也就是说native像JS发送消息的话,JS侧要先注册该方法registerHandler,native侧调用callHandler;
JS像native发送消息的话,native侧要先注册registerHandler,JS侧调用callHandler。这样才能完成双端通信。


如图:




native向JS发送消息


现在要从native侧向JS侧发送一条消息,方法名为:"OCToJSHandler",并且拿到JS的回调,具体实现细节如下:


JS侧


native向JS发送数据,首先要在JS侧去注册这个方法:

bridge.registerHandler('OCToJSHandler', function(data, responseCallback) {
...
})

这个registerHandler的实现在WebViewJavascriptBridge_JS是:

// web端注册一个消息方法,将注册的方法存储起来
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}

就是将这个注册的方法存储到messageHandlers这个map中,key为方法名称,value为function(data, responseCallback) {}这个方法。


native侧


native侧调用bridge的callHandler方法,传参为data和一个callback回调

id data = @{ @"dataFromOC": @"aaaa!" };
[_bridge callHandler:@"OCToJSHandler" data:data responseCallback:^(id response) {
NSLog(@"JS回调的数据是:%@", response);
}];

接下来会走到WebViewJavascriptBridgeBase的-sendData: responseCallback: handlerName:方法,该方法中将"data"和"handlerName"存入到一个message字典中,如果存在callback会生成一个callbackId一并存入到message字典中,并且将该回调存入到responseCallbacks中,key为callbackId,value为这个callback。代码如下:

// 所有信息存入字典
NSMutableDictionary* message = [NSMutableDictionary dictionary];
if (data) {
message[@"data"] = data;
}
if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}
if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];

将message存储到队列等待执行,执行该条message时会先将message进行序列化,序列化完成后将message拼接到字符串WebViewJavascriptBridge._handleMessageFromObjC('%@');中,然后执行_evaluateJavascript执行该js方法。

// 把OC消息序列化、并且转化为JS环境的格式,然后在主线程中调用_evaluateJavascript
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
[self _evaluateJavascript:javascriptCommand];
}

_handleMessageFromObjC方法会将messageJSON传递给_dispatchMessageFromObjC进行处理。
首先将messageJSON进行解析,根据handlerName取出存储在messageHandlers中的方法。如果该message中存在callbackId,将callbackId作为参数生成一个回调放到responseCallback中。
代码如下:

function _doDispatchMessageFromObjC() {
// 解析发送过来的JSON
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;

// 主动调用
// 如果有callbackid
if (message.callbackId) {
// 将callbackid当做callbackResponseId再返回回去
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
// 把消息从JS发送到OC,执行具体的发送操作
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};
// 获取JS注册的函数,取出消息里的handlerName
var handler = messageHandlers[message.handlerName];
// 调用JS中的对应函数处理
handler(message.data, responseCallback);
}
}

handler方法其实就是名为"OCToJSHandler"的方法,这时就走到了registerHandler里的那个function(data, responseCallback) {}方法了。我们看一下方法内部的具体实现:

bridge.registerHandler('OCToJSHandler', function(data, responseCallback) {
// OC中传过来的数据
log('从OC传过来的数据是:', data)
// JS返回数据
var responseData = { 'dataFromJS':'bbbb!' }
responseCallback(responseData)
})

data就是从native传过来的数据,responseCallback就是保存的回调,然后又生成了新数据作为参数给到了这个回调。


responseCallback的实现是:

responseCallback = function(responseData) {
// 把消息从JS发送到OC,执行具体的发送操作
_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};

将该方法的handlerName、生成的callbackResponseId(也就是callbackId)以及JS返回的数据一起给到_doSend方法。


_doSend方法将message存储到sendMessageQueue消息列表中,并使用messagingIframe加载了一次https://wvjb_queue_message

// 把消息从JS发送到OC,执行具体的发送操作
function _doSend(message, responseCallback) {
// 把消息放入消息列表
sendMessageQueue.push(message);
// 发出js对oc的调用,让webview执行跳转操作,可以在decidePolicyForNavigationAction:中拦截到js发给oc的消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

这时webview的监听方法decidePolicyForNavigationAction监听到了https://wvjb_queue_message 消息后还是执行WebViewJavascriptBridge._fetchQueue()去取数据,取到数据后根据responseId当初在_responseCallbacks中存储的callback,然后执行callback、移除responseCallbacks中的数据。到此为止,整个native向JS发送消息的过程就完成了。


总结:


  1. JS中先调用registerHandler将方法存储到messageHandlers中
  2. native调用callHandler:方法,将消息内容存储到message中,回调存储到responseCallbacks中。
  3. 将message消息序列化通过_evaluateJavascript方法执行_handleMessageFromObjC
  4. 将message解析,通过message.handlerName从messageHandlers取出该方法;根据message.callbackId生成回调
  5. 执行该方法,回调

JS向native发送消息


从JS向native发消息其实和native向JS发消息的接口层面是差不多的。


native侧


native侧首先要注册一个JSTOOCCallback方法

[_bridge registerHandler:@"JSTOOCCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
responseCallback(@"Response from JSTOOCCallback");
}];

该方法也同样是将该方法的callback存储起来,存储到messageHandlers当中,key就是方法名"JSTOOCCallback",value就是callback。


JS侧


JS侧会调用callHandler方法:

// 调用oc中注册的那个方法
bridge.callHandler('JSTOOCCallback', {'foo': 'bar'}, function(response) {
log('JS 取到的回调是:', response)
})

这个callHandler方法同样会调用_doSend方法:将callback存储到responseCallbacks中,key为callbakid;将消息存储到sendMessageQueue中;messagingIframe执行https://wvjb_queue_message


native的decidePolicyForNavigationAction方法监听到该消息后同样通过WebViewJavascriptBridge._fetchQueue()去取消息。


根据callbackId创建一个responseCallback,根据message的handlerName从messageHandlers取出该回调,然后执行:

WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}

WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];

handler(message[@"data"], responseCallback);

调用完这个方法后,该消息已经收到,然后将回调的内容回调给JS。
通过上面的代码可以看到,回调JS的内容就是callbackId和responseData生成的message,调用_queueMessage方法。


_queueMessage方法上面已经看过了,就是序列化消息、加入队列、执行WebViewJavascriptBridge._handleMessageFromObjC('%@');方法。


JS收到该消息后,处理返回的消息,从responseCallbacks中根据message中的responseId取出callback并且执行。最后删除responseCallbacks中的数据,JS向native发送数据就完成了。


总结:


  1. native侧调用registerHandler方法注册方法,方法名为JSTOOCCallback,将消息存储到messageHandlers中,key为方法名,value为callback。
  2. JS侧调用callHandler方法:将responseCallback存储到responseCallbacks中;将message存储到sendMessageQueue中;messagingIframe执行 http://wvjb_queue_message
  3. native侧监听到该消息后调用WebViewJavascriptBridge._fetchQueue()去取数据
  4. 根据handlerName从messageHandlers中取出该callback;根据callbackId创建callback对象作为参数放到handlerName的方法中;执行该回调。

总结


综上,WebViewJavascriptBridge的核心流程就分析完了,最核心的点是JS通过加载iframe来通知native侧;native侧通过evaluateJavaScript方法去执行JS。


从整个SDK来看,设计的非常好,值得借鉴学习:


  • 使用外观模式统一调用接口,比如初始化WebViewJavascriptBridge的时候,不需要关心使用方使用的是UIWebView还是WKWebView,内部已经处理好了。
  • 接口统一,不管是native侧还是JS侧,调用方法就是callHandler、注册方法就是registerHandler,不需要关注内部实现,使用非常方便。
  • 代码简洁,逻辑清晰,层次分明。从类的分布就能很清晰的看出各自的功能是什么。
  • 职责单一,比如decidePolicyForNavigationAction方法只负责监听事件、_fetchQueue是负责把消息转换成JSON字符串返回、_doSend是发送消息到native、_dispatchMessageFromObjC是负责处理从OC返回的消息等。虽然decidePolicyForNavigationAction也能接收消息,但这样就不会这么精简了。
  • 扩展性好,目前decidePolicyForNavigationAction虽然只有初始化和发消息两个事件,如果有其他事件还可以再扩展,这也得益于方法设计的职责单一,扩展对原有方法影响会很小。

作者:好_好先生
链接:https://juejin.cn/post/7168824876059328548
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

程序员如何把控自己的职业(陈皓--左耳朵耗子)

背景 最近,很多小伙伴留言说自己比较焦虑和迷茫。 这让我想起来之前看的 陈皓老师 的文章,这篇文章帮助我很多,也希望它能帮助更多的人。 因此转发~~ 正文 文章转发自《陈皓(左耳朵耗子)》:coolshell.cn/articles/20… 这篇文章的主要...
继续阅读 »

背景


最近,很多小伙伴留言说自己比较焦虑和迷茫。


这让我想起来之前看的 陈皓老师 的文章,这篇文章帮助我很多,也希望它能帮助更多的人。


因此转发~~


正文



文章转发自《陈皓(左耳朵耗子)》:coolshell.cn/articles/20…



这篇文章的主要内容主要是我今年3月份在腾讯做的直播,主要是想让一些技术人员对世界有一个大体的认识,并且在这个认识下能够有一个好的方法成就自己。而不是在一脸蒙圈的状态下随波逐流,而日益迷茫和焦虑。直播完后,腾讯方面把我的直播形成文字的形式发了出来,我觉得我可以再做一个精编版。所以,有了这篇文章,希望对大家有帮助。


对我来说,在我二十多年的工作经历来看,期间经历了很多技术的更新换代,整个技术模式、业务模式也是一直变来变去,我们这群老程序员成长中所经历的技术比今天的程序员玩的还更杂更多。我罗列一下我学过的,而且还被淘汰掉的技术,大家先感受一下。

- MIS应用开发:FoxPro,PowerBuilder,Delphi
- OA:Lotus Notes,VBScripts
- 微软:ODBC/ADO,COM/DCOM,MFC/ATL,J++
- 服务器:AIX,HP-UX,SCO Unix
- Web:CGI,ISAPI,SOAP
- RPC:CICS,Tuxedo
- J2EE:Websphere,Weblogic
- DB:Sybase,Informix

我想说的是,无论过去还是今天,我们这些前浪和你们后浪所面对的技术的挑战和对技术的焦虑感是相似的,我们那个时候不但玩996,还玩封闭开发(就是一周只能回家一天)。当然,唯一好的东西,就是比起今天的程序员来说,我们那个年代没有像微信、微博、知乎,抖音这些巨大消耗你人生的东西,所以,我们的工作、生活和成长都有很效率,不会被打断、喜欢看书、Google还没有被封……当然,那时代没有StackOverlow和Github这样的东西,所以,能完成的东西或质量都一般。


当然,这里并不是想做一个比较,只是想让大家了解一下两代程序员间的一些问题各有千秋,大同小异。在整个成长过程中,其实有很多东西是相通的,其本上来说,就是下面的三件事——


第一,如果想要把控技术,应对这个世界的一些变化,需要大致知道这个世界的一些规律和发展趋势,另外还得认识自己,自己到底适合做什么?在这个趋势和规律下属于自己的发挥领域到底是什么?这是我们每个人都需要了解的。


第二打牢基础,以不变应万变,不管世界怎样变化,我都能很快适应它。基础的重要程度对于你能够飞多高是相当有影响的,懂原理的人比不懂原理的人能做出来的事情或是能解决的问题完全是两个层级的。


第三,提升成长的效率,因为现在社会的节奏实在太快了,比二十年前快得太多,技术层出不穷,所以我们的成长也要更有效率。效率并不单指的快,效率是怎么样更有效,是有用功除以总功(参看《加班与效率》),怎么学到更有效的东西,或者怎么更有效学习,是我们需要掌握的另一关键。


下面是我这多年来的一些认识,希望对你有帮助。


目录


世界发展趋势


我个人经历的信息化革命应该分成三个阶段:


  • 1990年代到2000年,这个时代MB时代,是雅虎、新浪、搜狐、网易门户网站的时代,这个时代就是ISP/ICP互联网提供商,把一些资讯数字化,然后发布到网络上。
  • 2000年到2010年,这个时代叫GB时代,或是叫多媒体或UGC时代,上网开始变得普遍了,每个人手里的数码设备开始变得多了起来,可以上传照片,可以上传视频,甚至可以在网上做社交。
  • 2010年到2020年,这个时代叫TB时代,这过去的十年是移动互联网时代,移动互联网只需要手机在线,不需要依靠电脑。因为手机随时在线,所以个人的各种各样的数据始终在被收集,只要用户上网就会产生数据,所以人的行为最终也被数字化了。

所有的硬件和软件都是跟着需要处理的数据而演进的,我们需要更大的带宽,更大的硬盘,更多的处理器……大到一定时候就只能进入分布式化的技术架构了,再大,数据中心也顶不住了,就会要引入更为分布式的边缘计算了。


另一方面,从业务上来看,我们可以看到整个世界就在不断地进行数字化,因为,只要数字化了,就可以进行复制传播和计算,只要可以进行计算了,就可以进行数学建模,就可以自动化,只要可以自动化了就可以规模化,只要可能规模化了,就可以改变整个行业。人类的近代史的大趋势基本上都是在解决能源和自动化的事,源源不断的能源是让机器不知疲倦的前提条件,用机器代替牲口,代替人类进行工作是规模化的前提条件。


所以,技术的演进规律基本是自动化加规模化,从而降低成本,提升效率。这就是为什么世界变得越来越快,人类都快跟不上节奏的原因,主要是整个社会不断被机器、数据所驱动。


人才需求


在这个过程中,需要什么样的人?下面是我的一些认识——


  • 技工,在机器和自动化面前,肯定是需要能够操作机器的技术工人了,这类人是有技术的劳动力。在编程的圈子里俗称“码农”,他们并不是真正的工程师,他们只是电脑程序的操作员,所以,随着技术门槛的下降或是技术形式的变更他可能就会变得越来越不值钱,直到被淘汰掉
  • 特种工,这种人是必须了解原理和解决难题的一类人,他们是解决比较难的、特定的一些技术问题。当一种技术被淘汰,他并不容易被淘汰,因为他懂原理,原理就是解决问题的能力,是解决问题的套路和方法
  • 工程师,不但是使用技术,还可以把活儿做好,他们认为代码更多的时间是在维护,这些人使用各种各样的手段和各种技术,精益求精地持续不断地提高代码的易读性、扩展性、可维护性和重用性,这个过程似乎永无止境。对于这些有“洁癖”,有“工匠精神”,有“修养”的技术人员,我们称他们为工程师。这种人做事又稳又快,而且可以做出很多称手的工具和方法论
  • 再往上是设计师和架构人员,这些人主要是开发一些工具,框架,模式,提升软件开发和维护效率,同时也提升用户体验,和提升稳定性、性能、代码重用等,总的来说就是为了降本增效。这类人的工作降低了技术得到门槛,他们把技术门槛降低了以后,就可以把这个技术普及开来,就可以由广大劳工、技工、特殊工人使用了。
  • 还有一类人是经理,经理主要是组织团队、完成项目、创造利润。这类人中,即有身先士卒的leader,也有高高在上的boss,但无论怎么样,这些人只不过是为了让一个公司或是一个团队更好组织在一起的“粘合剂”,这类人只有在大公司中才会变成更有价值。

这就是我总结的世界需要哪些人才,我们了解这些东西以后大概就明白我们现在所处的位置有什么样的问题,我们应该去什么样的地方。


Google评分卡


接下来,我们再来看看Google的SRE的自我评分卡:



0 – 对于相关的技术领域还不熟悉

1 – 可以读懂这个领域的基础知识

2 – 可以实现一些小的改动,清楚基本的原理,并能够在简单的指导下自己找到更多的细节。


3 – 基本精通这个技术领域,完全不需要别人的帮助

4 – 对这个技术领域非常的熟悉和舒适,可以应对和完成所有的日常工作。



  • 对于软件领域 – 有能力开发中等规模的程序,能够熟练和掌握并使用所有的语言特性,而不是需要翻书,并且能够找到所有的冷知识。

  • 对于系统领域 – 掌握网络和系统管理的很多基础知识,并能够掌握一些内核知识以运维一个小型的网络系统,包括恢复、调试和能解决一些不常见的故障。


5 – 对于该技术领域有非常底层的了解和深入的技能。


6 – 能够从零开发大规模的程序和系统,掌握底层和内在原理,能够设计和部署大规模的分布式系统架构

7 – 理解并能利用高级技术,以及相关的内在原理,并可以从根本上自动化大量的系统管理和运维工作。

8 – 对于一些边角和晦涩的技术、协议和系统工作原理有很深入的理解和经验。能够设计,部署并负责非常关键以及规模很大的基础设施,并能够构建相应的自动化设施


9 – 能够在该技术领域出一本经典的书。并和标准委员会的人一起工作制定相关的技术标准和方法。

10 – 在该领域写过一本书,被业内尊为专家,并是该技术的发明人。



SRE需要自评如下这些技术或技能。



– TCP/IP Networking (OSI stack, DNS etc)

– Unix/Linux internals

– Unix/Linux Systems administration

– Algorithms and Data Structures

– C/C++

– Python

– Java

– Perl

– Go

– Shell Scripting (sh, Bash, ksh, csh)

– SQL and/or Database Admin

– Scripting language of your choice (not already mentioned) _____________

– People Management

– Project Management



这个评分卡是面试Google前需要候选人对自己的各种技术进行自评,也算是一种技术人员的等级的度量尺,其把技术的能分成11个等级,我用颜色把其它成四大层级,希望这个评份卡能够给你一个能力提升的参考标准。


认识自己


认识了世界是怎么发展的,也知道技术人员的种类和层级,那么还要了解一下自己,因为如果不了解自己,那么你也无法找到自己的路和适合自己的地方。


我觉得,一个人要认识自己就需要认识自己的特长、兴趣、热情、擅长等,下面是一个认识自己的标准方法:


  • 特长。首先你要找得到自己特长。你要认识自己的特长,找到自己的天赋,找到你在DNA里比别人强的东西,就拿你的DNA跟别人竞争就好了。所以你要找到自己可以干成的事,找到别人找你请教的事,你身边人找你请教就是说明你有特长。这是找到自己特长非常非常重要,扬长避短。
  • 兴趣。如果你没有找到自己特长,就找自己有兴趣有热情的东西。什么叫兴趣?兴趣是再难再累都不会放弃的事。如果你遇到困难就会放弃不叫兴趣,那叫叶公好龙。不怕困难,痴迷其中,就算你没有特长,有了这种特质,你也是头部的人才。
  • 方法。如果你没有特长,没有兴趣和热情就要学方法。这种方法就是要有时间观念,要会做计划,要懂统筹、规划对于做过的事情,犯过的错误多总结,举一反三,喜欢自己找答案,自己探究因果关系,这是一些方法,自己总结一些套路。
  • 勤奋。 如果你没有特长,没有兴趣,也没有方法,你还能做的事就是勤奋,勤奋注定会让你成为一个比较劳累的人,也是很有可能被淘汰的人随着你的年纪越来越大,你的勤奋也会越来越不值钱。因为年轻人会比你更勤奋,比你更勤奋、比你斗志更强,比你能力更强,比你要钱更少的人会出现。勤奋最不值钱,但是只要你勤奋至少能够自食其力。

以上就是为了应对未来技术变化,作为个人必须要从特长、兴趣、方法一层一层筛选挖掘,如果没有这些你就要努力和勤奋。就只能接受“福报”了


从我个人而言,我不算是特别聪明的人,但自认为对技术还是比较感兴趣的,难的我不怕。有很多比较难啃的技术,聪明点的人啃一个月就懂了,我不行,我可能啃半年。但是没有关系,知识都是死的,只要不怕困难总有一天会懂的。最可怕是畏难,为自己找借口,这样就不太好了。


打好基础


最前面提到我学的各式各样的被淘汰的技术,会让你感觉很迷茫,或是迷失。但前面也提到了“谷歌评分卡”,在这个评分卡中,我们看到了许多基础原理方面的内容,其实要应对未来的变化,很重要的一点就是无招胜有招,以不变应万变。


变化都是表面的东西,内在的东西其实并没有太多的变化。理论层面上变得不多,反而形式上的东西今天一个花样,明天一个花样,所以如果要去应对这种变化,就一定要打牢自己的基础,提升内功修养。比如像编程的一些方式和套路,修饰模式原理本质,解耦,提升代码的重用度等。提升代码重用度必须解耦,要跟现实解耦,提升抽象,这些都是一些技术基础。无论用什么语言,都是这么做的。


打牢基础就可以突破瓶颈,不打牢基础没有办法突破瓶颈。在技术世界不要觉得量变会造成质变,这是不可能的。技术这个东西就像搞建筑砌砖头,砌砖头砌的再多也不可能让你能成为一个架构师的,因为你不懂原理,不懂科学方法,你就不可能成长上去的,就像学数学一样,当你掌握了微积分这种大杀器后,你解题的能力是无所披靡,而微积分这种方式绝对不是你能“量变”出来的。


所以你必须学习基础的理论知识,如果不学这些基础理论知识,还要学习解题思路和方法,如果你只学在表面,那么当这个技术的形式有变化,就会发现以前学的都没用了,要重头学一遍。掌握技术基础可以让自己找到答案和知识,基础是抽象和归纳,很容易形成进一步的推论。我们学的很多技术实现都逃不脱基础原理,不管是Java,还是其他语言,只要用TCP用的都是相同的原理,逃不出范围,只要抓住原理,举一反三,时间一长了,甚至还可以自己推导答案。对于技术的基础,我会把其它成四类:


  • 程序语言:语言的原理,类库的实现,编程技术(并发、异步等),编程范式,设计模式……
  • 系统原理:计算机系统,操作系统,网络协议,数据库原理……
  • 中间件:消息队列,缓存系统,网关代理,调度系统 ……
  • 理论知识:算法和数据结构,数据库范式,网络七层模型,分布式系统……

这些知识其实就是一个计算机科学专业的学生他所要学习的原理,但可惜的是,我们的一些学校教得也很糟糕,不但老师能力不足,而且放着世界上最优秀的教课书不用了,一定要自己写一本。讲也讲不全,还有各种错误,哎……总之,如果你学习用用到的教材不行,那么可以肯定的是你的学习效率一定是很糟糕的。这就是为什么我们大学上完了,还是跟个傻瓜一样,还要在工作中再重新自学。


不过,就算自学,这些基础技术大概需要四五年的时间堆叠。我工作二十年了,这二十年来基本还是这些原理没变,无论形式怎么变,但是核心永远还是这些,理论创新很难,这是以不变应万变


学习效率


谈到学习效率,就需要拿出这张学习金字塔的图来了。从图可以看到学习方法分布两层,一种是被动学习,也是浅度学习,听讲,阅读,视听,演示都是在被动学习,而与人讨论,自己动手实践,教授给别人是主动学习。主动学习我们称之为深度学习,如果你不能深度学习,你就不能真正学到东西。这也是你会经常有“学那么多干什么,不用就忘了”,这就是浅度学习的症状了。


下面,我给出一些我自己觉得不错的学习经验:


1、挑选一手知识和信息源。 对于学习方法:第一我们一定要到知识源去挑选知识,知识信息源非常关键,二手信息丢失太大了,谭浩强写的书就丢失太多信息了。目前计算机一手知识基本都是国外的,所以英文非常重要。我鼓励大家一定读第一手的资料。如果你英语有问题,至少要看翻译过来,最好是原汁原味翻译的,不要我理解了给你讲那种,那种也是被别人嚼一遍再讲给你你没有体会,是别人带着你,别人的体会会影响你,也许你的体会会比他更好,因为是你自己总结出来的东西,所以知识源很重要。


2、注意原理和基础 第二要注重基础原理。虽然可以忘记这个技术,但是原理记在心里,我可以徒手实现出来,而且通过原理可以更快学习其他类似的技术。所以原理很重要!当你学会C、C++要学Java和GO都很快。


3、使用知识图谱 一定要学会使用知识图,把知识结构化。从一个技术关键点开始不断地关联和细化下去,比如:关于TCP协议,首先第一个要记住状态图,怎么建立连接,怎么断连接,状态怎么变迁。TCP没有连接,是靠状态维护连接的。其次,要了解TCP怎么保证可靠性,就是丢包以后怎么重传,重传有哪些技术点。然后,重传会让你联想到拥塞控制,拥塞控制到滑动窗口……。这基本就是TCP的所有东西了,找到关键点,然后顺着这个脉络一点点往下想,通过知识图关联就可以进行顺藤摸瓜。我们不需要记所有知识,那些手册的知识不需要记,你知道在哪里能找到就可以了。你脑子里面要有地图,学一个东西就跟在城市生活一样,闭上眼睛就知道地图,A点到B点怎么去大概方向要知道。我在北京我去广州,广州在南边,我大概坐飞机还是火车要心里有数。。


4、学会举一反三。就是用不同方法学一个东西,比如说学TCP协议,看书是一种方法,编程是另外一种方法,还有用做Debug去看的,用不同方法学一个东西会让你更加熟悉,你学一个知识的同时把周边也学了。比如说学前端能不能把HTTP学一下,比如说长连接、短连接,包括hp1、hp2有一些不一样的东西。


5、总结和归纳。 只有学会总结和归纳,才能形成自己的思维框架、自己的套路、自己的方法论,以后学这个东西应该怎么学。就像学一门新的语言,不管GO语言,还是Rust语言,第一件事情就是了解内存是怎么管理的,数据类型什么样,第二是泛型怎么搞,第三是并发怎么弄。还有一些抽象怎么弄,比如说怎么解耦,怎么实现多态?套路这种东西只有学的多了以后才能形成套路,如果你只学会一门语言不会有套路,你要每年学门语言,不用学多精,你思考这个语言有什么不一样,为什么这个这种有玩法,那个有那种玩法,这些东西思考多了套路方法论就出来了。比如说Windows和Linux有什么不同,Linux和Unix又有什么不同?只有总结自己的框架、套路和方法,这些才永远不会被淘汰。


6、实践和坚持。 剩下就是多做多练,多坚持,只有实践才会有经验,只有锻炼了才能够把自己的脂肪变没,所以,要把知识变成技能必须练,就像小学生学会加减乘除,还是要演练,必须多做题,题目做得多了,自然掌握得好。要挑选好的知识源,注重原理技术,有一些原理的基础的书太枯燥,但是我告诉你学习这些基础太值得投入时间,搬砖赚几十元不值得,因为赚的是辛苦钱,老了就赚不了,必须要赚更有能力的钱,这是学习投资。


小结


好了,该到这篇文章收尾的时候了,小结一下,如果你想更好的把握时代,提升自己,你需要知道这个时代的趋势是什么,需要什么样的人,这些人需要什么样的能力,这些能力是怎么获得的,投入到基础知识的学习就像“基建”一样,如果基础不好,不能长高,学习能力也是需要适应这个快速时代的重要的基础能力,没有好的学习能力,很快就会掉队被淘汰。


这些东西,是我从业二十年来的总结和体会,希望对你有用。


作者:程序员Sunday
链接:https://juejin.cn/post/7249281117168861240
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

世界那么大,我并不想去看看

回家自由职业有一个半月了,这段时间确实过的挺开心的。 虽然收入不稳定,但小册和公众号广告的收入也足够生活。 我算过,在小县城的家里,早饭 10 元,午饭 60元(按照偶尔改善生活,买份酸菜鱼来算),晚饭 10 元,牛奶零食等 20 元,一天是 100 元。 一...
继续阅读 »

回家自由职业有一个半月了,这段时间确实过的挺开心的。


虽然收入不稳定,但小册和公众号广告的收入也足够生活。


我算过,在小县城的家里,早饭 10 元,午饭 60元(按照偶尔改善生活,买份酸菜鱼来算),晚饭 10 元,牛奶零食等 20 元,一天是 100 元。


一年就是 36500 元。


加上其他的支出,一年生活费差不多 5w 元。


10 年是 50w,40 年就是 200 万元。


而且还有利息,也就是说,我这辈子有 200 万就能正常活到去世了。


我不会结婚,因为我喜欢一个人的状态,看小说、打游戏、自己想一些事情,不用迁就另一个人的感受,可以完全按照自己的想法来生活。


并不是说我现在有 200 万了,我的意思是只要在二三十年内赚到这些就够了。


我在大城市打工,可能一年能赚 50 万,在家里自由职业估计一年也就 20 万,但这也足够了。


而且自由职业能赚到最宝贵的东西:时间。


一方面是我自己的时间,我早上可以晚点起、晚上可以晚点睡、下午困了可以睡个午觉,可以写会东西打一会游戏,不想干的时候可以休息几天。


而且我有大把的时间可以来做自己想做的事情,创造自己的东西。


一方面是陪家人的时间,自从长大之后,明显感觉回家时间很少了,每年和爸妈也就见几天。


前段时间我爸去世,我才发觉我和他最多的回忆还是在小时候在家里住的时候。


我回家这段时间,每天都陪着我妈,一起做饭、吃饭,一起看电视,一起散步,一起经历各种事情。


我买了个投影仪,很好用:



这段时间我们看完了《皓镧传》、《锦绣未央》、《我的前半生》等电视剧,不得不说,和家人一起看电视剧确实很快乐、很上瘾。


再就是我还养了只猫,猫的寿命就十几年,彼此陪伴的时间多一点挺好的:



这些时间值多少钱?没法比较。


回家这段时间我可能接广告多了一点,因为接一个广告能够我好多天的生活费呢。


大家不要太排斥这个,可以忽略。


其实我每次发广告总感觉对不起关注我的人,因为那些广告标题都要求起那种博人眼球的,不让改,就很难受。



小册的话最近在写 Nest.js 的,但不只是 nest。


就像学 java,我们除了学 spring 之外,还要学 mysql、redis、mongodb、rabbitmq、kafka、elasticsearch 等中间件,还有 docker、docker compose、k8s 等容器技术。


学任何别的后端语言或框架,也是这一套,Nest.js 当然也是。


所以我会在 Nest.js 小册里把各种后端中间件都讲一遍,然后会做 5 个全栈项目。


写完估计得 200 节,大概会占据我半年的时间。


这段时间也经历过不爽的事情:





这套房子是我爸还在的时候,他看邻居在青岛买的房子一周涨几十多万,而且我也提到过可能回青岛工作,然后他就非让我妈去买一套。


当时 18 年青岛限购,而即墨刚撤市划区并入青岛,不限购,于是正好赶上房价最高峰买的。


然而后来并没有去住。


这套房子亏了其实不止 100 万。


因为银行定存利息差不多 4%,200 万就是每年 8万利息,5年就是 40万。


但我也看开了,少一百万多一百万对我影响大么?


并不大,我还是每天花那些钱。


相比之下,我爸的去世对我的打击更大,这对我的影响远远大于 100 万。


我对钱没有太大的追求,对很多别人看重的东西也没啥追求。


可能有的人有了钱,有了时间会选择环游中国,环游世界,我想我不会。


我就喜欢宅在家里,写写东西、看看小说、打打游戏,这样活到去世我也不会有遗憾。


我所追求的事情,在我小时候可能是想学医,一直觉得像火影里的纲手那样很酷,或者像大蛇丸那样研究一些东西也很酷。


但近些年了解了学医其实也是按照固定的方法来治病,可能也是同样的东西重复好多年,并不是我想的那样。


人这一辈子能把一件事做好就行。


也就是要找到自己一生的使命,我觉得我找到了:我想写一辈子的技术文章。


据说最高级的快乐有三种来源:自律、爱、创造。


写文章对我来说就很快乐,我想他就是属于创造的那种快乐。


此外,我还想把我的故事写下来,我成长的故事,我和东东的故事,那些或快乐或灰暗的日子,今生我一定会把它们写下来,只是现在时机还不成熟。


世界那么大,我并不想去看看。


我只想安居一隅,照顾好家人,写一辈子的技术文章,也写下自己的故事。


这就是我的平凡之路。


作者:zxg_神说要有光
链接:https://juejin.cn/post/7233327509919547452
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【JD京东抢购】茅台抢购逻辑

直接进入正文。京东抢购模式有很多种。 普通商品无货,定时查询库存蹲抢普通商品定时发售(库存由0变为有货),定时提前构造订单请求抢预售商品(需要先预约),可以加入购物车,通过购物车结算。这种用常规购物车结算订单接口就行,当然也可以用抢购接口。 这种体现为可以加...
继续阅读 »

直接进入正文。京东抢购模式有很多种。


  1. 普通商品无货,定时查询库存蹲抢
  2. 普通商品定时发售(库存由0变为有货),定时提前构造订单请求抢
  3. 预售商品(需要先预约),可以加入购物车,通过购物车结算。这种用常规购物车结算订单接口就行,当然也可以用抢购接口。


这种体现为可以加购,抢购时候显示两个按钮,加入购物车(黄色)和立即购买(淡绿色)。



  • 预售商品(需要先预约),无法加入购物车,电脑端无法预约,必须手机端预约。这种采用marathon.jd.com/seckillnew/… 接口完成抢购,有完整流程验证和tokenKey(sign),sk验证。


这种体现为 无法加入购物车,必须手机端才能预约,可购买时候只显示一个红色按钮立即抢购



逻辑参考GitHub大佬给出的思路。


第一步:获取跳转链接


跳转链接是指形如:un.m.jd.com/cgi-bin/app… 的链接,获取该链接,还需要一个前置步骤,即获取token和拼接url。先说获取token,获取token是通过genToken接口获取的,然后将获取到的tokenKey和url拼接起来,得到跳转链接。


第二步:访问跳转链接


拿到跳转链接后,直接将该跳转链接仍给浏览器即可,浏览器会经过两次302跳转得到sekill.action链接,从而渲染出提交订单页面,此时我们需要模拟点击“提交订单”按钮,实现抢购。(可以使用Selenium、Pyppeteer或Playwright等类库 来模拟浏览器)


访问跳转连接,及提交订单的时候需要提供移动端的APP参数抓包获取。Android抓包较为简单,IOS的也不麻烦,就是步骤多了一些。


然后提取Hades头的信息组成以下参数

        query_params = {
"functionId": "genToken",
"clientVersion": "12.0.8",
"build": "168782",
"client": "apple",
"d_brand": "apple",
"d_model": "iPhone11,4",
"osVersion": "16.5",
"screen": "1284*2778",
"partner": "apple",
"aid": self.aid,
"eid": self.eid,
"sdkVersion": "29",
"lang": "zh_CN",
# 'harmonyOs': '0',
"uuid": self.uuid,
"area": "4_51026_58465_0",
"networkType": "wifi",
"wifiBssid": self.wifiBssid,
"uts": self.uts,
"uemps": "0-0-0",
"ext": '{"prstate":"0","pvcStu":"1"}',
# 'ef': '1',
# 'ep': json.dumps(ep, ensure_ascii=False, separators=(',', ':')),
}

这种仅仅是前面所需的参数。具体方法还是需要使用这些参数来获取用户的个人信息拿到跳转连接


如:

    def get_appjmp(self, token_params):
headers = {"user-agent": self.ua}
appjmp_url = token_params["url"]
params = {
"to": "https://divide.jd.com/user_routing?skuId=%s" % self.skuId,
"tokenKey": token_params["tokenKey"],
}

response = self.s.get(
url=appjmp_url,
params=params,
allow_redirects=False,
verify=False,
headers=headers,
)
print("Get Appjmp跳转链接-------------->%s" % response.headers["Location"])
return response.headers["Location"]

get_appjmp(self, token_params) 函数接受一个名为 token_params 的参数。


然后发送相关请求后携带参数得到跳转链接。

  • headers 是一个字典,包含了请求头中的 "User-Agent" 字段,用于模拟浏览器的用户代理。

  • appjmp_url 是一个变量,它存储了 token_params 字典中的 "url" 键所对应的值。

  • params 是一个字典,其中包含两个键值对:

  • "to" 键对应的值是一个字符串,使用了 %s 占位符,用于生成跳转链接中的 skuId 参数。

  • "tokenKey" 键对应的值是一个字符串,使用了 token_params 字典中的 "tokenKey" 键所对应的值。

  • 通过调用 self.s.get() 方法发起一个 GET 请求,传入以下参数:

  • url 参数是 appjmp_url,表示要访问的链接地址。

  • params 参数是之前定义的 params 字典,用于添加请求参数。

  • allow_redirects 参数设置为 False,禁止自动重定向。

  • verify 参数设置为 False,跳过 SSL 证书验证。

  • headers 参数是之前定义的 headers 字典,用于设置请求头。

  • 最后,打印获取到的跳转链接的响应头中的 "Location" 字段值,并将其返回。


抢购返回解决无非就是



{'errorMessage': '很遗憾没有抢到,再接再厉哦。', 'orderId': 0, 'resultCode': 90016, 'skuId': 0, 'success': False}




{'errorMessage': '很遗憾没有抢到,再接再厉哦。', 'orderId': 0, 'resultCode': 90008, 'skuId': 0, 'success': False}





根据其他作者的推测 推测返回 90008 是京东的风控机制,代表这次请求直接失败,不参与抢购。

小白信用越低越容易触发京东的风控。


具体代码可参考GitHub地址


感谢GitHub作者@geeeeeeeek @jd-seckill等


作者:狗头大军之江苏分军
链接:https://juejin.cn/post/7280740005571362816
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

音频播放器-iOS

iOS
AudioPlaybackManager 该音频播放器基于 AVPlayer 实现在线/本地播放, 在线播放支持加载本地缓存。支持设置后台播放信息。支持远程控制。 可初始化、可单例。兼容 OC 调用。 代码结构  AudioPlaybackManag...
继续阅读 »

AudioPlaybackManager


该音频播放器基于 AVPlayer 实现在线/本地播放, 在线播放支持加载本地缓存。支持设置后台播放信息。支持远程控制。


可初始化、可单例。兼容 OC 调用。


代码结构



 AudioPlaybackManager 为实现基础播放类, 其余功能则分别位于不同文件中, 下边会根据该目录结构来进行对应功能的简单使用讲解。


播放设置


基础播放


设置 playerItem

let audio = Audio(audioURL: URL)
AudioPlaybackManager.shared.setupItem(audio, beginTime: 0.0)

针对 playerItem 添加了 3 个监听, 分别是:

  1. AVPlayerItem.status, 监听 playerItem 状态。

    • 当处于 readyToPlay 状态时, 会在此处获取音频总时长, 同时若 autoPlayWhenItemReady = true 时, 则会自动播放。

      若需要手动播放, 则可在收到 AudioPlaybackManager.readyToPlayNotification 通知或 audioPlaybackManagerRreadyToPlay(_:) 代理方法之后调用 play() 方法即可。

  2. AVPlayerItem.loadedTimeRanges, 监听缓存加载进度, 同步至 loadedTime

  3. AVPlayerItemDidPlayToEndTime, 监听播放完成, 同步至 playStatus = .playCompleted


属性监听


  • @objc dynamic var playStatus: PlayStatus = .prepare
enum PlayStatus: Int {
   case prepare, playing, paused, stop, playCompleted, error
}
  • @objc dynamic var playTime: Float64 = 0

    • 默认为 (1/30)s 回调 1 次
  • @objc dynamic var progress: Float = 0

    • 默认为 (1/30)s 回调 1 次
  • @objc dynamic var duration: Float64 = 0

  • @objc dynamic var loadedTime: Float64 = 0


以上属性均支持通过 KVO 监听。


播放控制

  • play()

  • pause()

  • togglePlayPause()

  • stop()

  • switchNext()

    • 收到 AudioPlaybackManager.nextTrackNotification 通知或 audioPlaybackManagerNextTrack(_:) 代理方法后重新设置 setupItem(_:beginTime:)
  • switchPrevious()

    • 收到 AudioPlaybackManager.previousTrackNotification 通知或 audioPlaybackManagerPreviousTrack(_:) 代理方法后重新设置 setupItem(_:beginTime:)

更多控制


  • skipForward(_ timeInterval: TimeInterval)
  • skipBackward(_ timeInterval: TimeInterval)
  • seekToPositionTime(_ positionTime: TimeInterval)
  • seekToProgress(_ value: Float)
  • beginRewind(rate: Float = -2.0)
  • beginFastForward(rate: Float = 2.0)
  • endRewindFastForward()

播放被其他 App 影响


中断


当电话、闹钟、其它非官方 App 播放(这里涉及到后台播放, 下边会讲)... 时, 若二者不支持混音播放, 那么当前播放则会被系统暂停。这里主动调用了 pause() 来跟随变更播放状态。


中断恢复播放


var shouldResumeWhenInterruptEnded = true, 若不期望自动恢复播放, 可将其置为 false


若中断方在结束播放后告知系统应该通知其他应用程序其已经停用了音频会话, 那么被中断的音频会话则可以选择是否继续播放。


一般系统 App 都会对此进行通知, 而部分第三方 App 可能没对此进行处理, 那么也将不能自动恢复播放。


ps: 由于目前没有混音播放的需求, 后续考虑是否要将中断通知转发给开发者来自主控制暂停/播放。


播放 Route 变更


外设变更涉及:


  1. 从外音播放改为耳机播放,继续播放;
  2. 耳机播放中,拿掉耳机(AirPods)自动暂停, 戴上继续播放;

总体可以概括为:

switch reason {
   case .newDeviceAvailable:
       play()
   case .oldDeviceUnavailable:
       pause()
   default: break
}

ps: 其他情况收到 route 变化通知如 AVAudioSession.Category 变更, 则不在该播放器考虑范畴内。


后台播放


  1. 开启后台播放权限

    1. 设置 setActiveSession(_ enabled: Bool)


在播放时设置为 true, 播放结束后设置为 false。如果仅在一个特定的控制器内播放的话, 在执行 deinit 方法中设置为 false 也是个不错的选择。


ps: 该方法设置 AVAudioSession.Category = .playback, AVAudioSession.Mode = .default。会保持应用程序音频在设备静音或屏幕锁定时能够继续播放。


在线播放加载本地缓存


var cacheEnabled: Bool, 提供了在线播放缓存开关, 默认关闭状态。


ps: 在线播放缓存引用了 VIMediaCache 第三方库, 支持自定义缓存目录, 默认存储在 tmp 目录下。想详细了解缓存流程的可以去看下, 文章写的很详细。


设置后台播放信息展示


var allowSetNowPlayingInfo: Bool, 默认为开启状态。


如需展示, 需要在设置 let audio = Audio(audioURL: URL) 时额外对其后台展示信息相关参数进行设置。


如需获取音频自身音频数据来进行展示, 则设置 useAudioMetadata = true 即可。


若音频不存在相关元数据, 则可以通过其他相关参数来进行设置。

    /// Audio url.
    open var audioURL: URL

    public init(audioURL: URL) {
        self.audioURL = audioURL
    }

    /// -------------- `MPNowPlayingInfoCenter` --------------

    /// Set `nowPlayingInfo` using audio metadata.
    ///
    /// Default is `false`.
    open var useAudioMetadata: Bool = false

    // Note: If `useAudioMetadata` is set to false, then you can set it through the following properties.

    /// Audio name.
    open var title: String?
    /// Album name.
    open var albumName: String?
    /// Artist.
    open var artist: String?

    /// Artwork.
    open var artworkImage: UIImage?
    open var artworkURL: URL?

ps: allowSetNowPlayingInfo = true 时播放进度相关信息会跟随一并设置。


效果图:




远程控制


简单远程控制方式

UIApplication.shared.beginReceivingRemoteControlEvents()
UIApplication.shared.endReceivingRemoteControlEvents()

AppDelegate 中实现

func remoteControlReceived(with event: UIEvent?) {
if let event = event, event.type == .remoteControl {
       switch event.subtype {
case .remoteControlPlay:
case ...
       }
   }
}

这种远程控制可满足大部分需求, 并且实现非常简单, 但是存在一个很大的问题, 就是无法实现进度条控制。


项目远程控制方式


采用 MPRemoteCommandCenter 方式。


基础控制功能

activatePlaybackCommands(_ enabled: Bool)
activatePreviousTrackCommand(_ enabled: Bool)
activateNextTrackCommand(_ enabled: Bool)
activateChangePlaybackPositionCommand(_ enabled: Bool)

长按 快进/快退


var remoteControlRewindRate: Float, 默认为 -2.0;


var remoteControlFastForwardRate: Float, 默认为 2.0;

activateSeekBackwardCommand(_ enabled: Bool)
activateSeekForwardCommand(_ enabled: Bool)

跳跃播放

复制代码
activateSkipForwardCommand(_ enabled: Bool, interval: Int = 0)
activateSkipBackwardCommand(_ enabled: Bool, interval: Int = 0)

ps: 开启跳跃播放会占用 上一首/下一首 位置。


关闭远程控制


在不需要远程控制功能时, 调用 deactivateAllRemoteCommands() 即可完全关闭。




项目


好了, 以上基本就是全部使用方法了。源代码及 Demo 可访问 Github 进行查看。


作者:cattt
链接:https://juejin.cn/post/7232256083875430461
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 网速检测方案

iOS
背景 为了基于网络状况做更细致的业务策略,需要一套网速检测方案,尽量低成本的评估当前网络状况,所以我们希望检测数据来自于过往的网络请求,而不是专门耗费资源去网络请求来准确评估。 指标计算 一般 RTT 作为网速的主要评估指标,拿到批量的历史请求 RTT 值后,...
继续阅读 »

背景


为了基于网络状况做更细致的业务策略,需要一套网速检测方案,尽量低成本的评估当前网络状况,所以我们希望检测数据来自于过往的网络请求,而不是专门耗费资源去网络请求来准确评估。


指标计算


一般 RTT 作为网速的主要评估指标,拿到批量的历史请求 RTT 值后,要如何去计算得到较为准确的目标 RTT 值呢?


影响 RTT 值的变量主要是:


  1. 网络状况会随时间变化;
  2. 请求来自不同的服务器,性能有差异,容易受到长尾数据影响;

首先参考 Chrome 的 nqe 源码:chromium.googlesource.com/chromium/sr…


权重设计


查阅相关源码后,发现历史请求的 RTT 值会关联一个权重,用于最终的计算,找到计算 RTT 权重的核心逻辑:

void ObservationBuffer::ComputeWeightedObservations(
const base::TimeTicks& begin_timestamp,
int32_t current_signal_strength,
std::vector<WeightedObservation>* weighted_observations,
double* total_weight) const {

base::TimeDelta time_since_sample_taken = now - observation.timestamp();
double time_weight =
pow(weight_multiplier_per_second_, time_since_sample_taken.InSeconds());

double signal_strength_weight = 1.0;
if (current_signal_strength >= 0 && observation.signal_strength() >= 0) {
int32_t signal_strength_weight_diff =
std::abs(current_signal_strength - observation.signal_strength());
signal_strength_weight =
pow(weight_multiplier_per_signal_level_, signal_strength_weight_diff);
}

double weight = time_weight * signal_strength_weight;


可以看到权重主要来自两个方面:


  1. 信号权重:与当前信号强度差异越大的 RTT 值参考价值越低;
  2. 时间权重:距离当前时间越久的 RTT 值参考价值越低;

这个处理能减小网络状况随时间变化带来的影响。


半衰期设计


在计算两个权重的时候都是用pow(衰减因子, diff)计算的,那这个“衰减因子”如何得到的呢,以时间衰减因子为例:

double GetWeightMultiplierPerSecond(
const std::map<std::string, std::string>& params) {
// Default value of the half life (in seconds) for computing time weighted
// percentiles. Every half life, the weight of all observations reduces by
// half. Lowering the half life would reduce the weight of older values
// faster.
int half_life_seconds = 60;
int32_t variations_value = 0;
auto it = params.find("HalfLifeSeconds");
if (it != params.end() && base::StringToInt(it->second, &variations_value) &&
variations_value >= 1) {
half_life_seconds = variations_value;
}
DCHECK_GT(half_life_seconds, 0);
return pow(0.5, 1.0 / half_life_seconds);
}

其实就是设计一个半衰期,计算得到“每秒衰减因子”,比如这里就是一个 RTT 值和当前时间差异 60 秒则权重衰减为开始的一半。延伸思考一下,可以得到两个结论:


  1. 同等历史 RTT 值量级下,半衰期越小,可信度越高,因为越接近当前时间的网络状况;
  2. 同等半衰期下,历史 RTT 值量级越大,可信度越高,因为会抹平更多的服务器性能差异;

所以更进一步的话,半衰期可以根据历史 RTT 值的量级来进行调节,找到它们之间的平衡点。


加权算法设计


拿到权值后如何计算呢,我们最容易想到的是加权平均值算法,但它同样会受长尾数据的影响。


比如当某个 RTT 值比正常值大几十倍且权重稍高时,加权平均值也会很大,更优的做法是获取加权中值,这也是 nqe 的做法,伪代码为:

//按 RTT 值从小到大排序
samples.sort()
//目标权重是总权重的一半
desiredWeight = 0.5 * totalWeight
//找到目标权重对应的 RTT 值
cumulativeWeight = 0
for sample in samples
cumulativeWeight += sample.weight
If (cumulativeWeight >= desiredWeight)
return sample.RTT

进一步优化


通过历史网络请求样本数据计算加权中值,根据计算后的 RTT 值区间确定网速状态供业务使用,比如 Bad / Good,这种策略能覆盖大部分情况,但有两个特殊情况需要优化。


无网络访问场景


当用户一段时间没有访问网络缺乏样本数据时,引入主动探测策略,发起请求实时计算 RTT 值。


网络状况快速劣化场景


若在某一个时刻网络突然变得很差,大量请求堆积在队列中,由于我们 RTT 值依赖于网络请求落地,这时计算的目标 RTT 值具有滞后性。


为了解决这个问题,可以记录一个“未落地请求”的队列,每次计算 RTT 值之前,前置判断一下“超过某个阈值”的未落地请求“超过某个比例”,视为弱网状态,达到快速感知网络劣化的效果。


作者:波儿菜
链接:https://juejin.cn/post/7171020344117952542
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

被裁后的一天

距我被裁已经过去 34 天了,我还没有去找工作,接下来的一个月大概率也不会找工作,而是打算和老公去西藏旅游一趟,我对一周或者两周后的西藏之旅满是期待。至于老东家,我对他没有怨言,在那儿工作期间很愉快,被裁的时候他爽快的给了 N+1 的赔偿,离职的过程中没有发生...
继续阅读 »

距我被裁已经过去 34 天了,我还没有去找工作,接下来的一个月大概率也不会找工作,而是打算和老公去西藏旅游一趟,我对一周或者两周后的西藏之旅满是期待。至于老东家,我对他没有怨言,在那儿工作期间很愉快,被裁的时候他爽快的给了 N+1 的赔偿,离职的过程中没有发生不愉快的事情。我写这篇文章不是为了缅怀过去或者反思过去,而是记录我的一天,余下的内容没有华丽的词藻,也没有令人深思的道理,只是流水账。


今天我 7 点 20 就醒了,醒来的前一刻还在做梦,我梦到自己睡不着觉,可想而知那不是一个美梦。醒来的那一瞬间,我甚至没有分辨出是现实还是梦境,头昏昏沉沉的赖了一会儿床,在赖床期间我老公起床了,他洗漱结束的时候,我用手机放了一首歌,他走到床边对我巴拉巴拉说了些话,说的都是些大道理,大概是选择呀未来呀思考呀这一套,如果让我将他的话背下来,可真是在难为我。其实他一大早就讲大道理,我特别不想听,但我觉得他的话有些道理,于是赶紧起了床,洗漱完去跑步。


今天温度不高,早上很适合跑步,估摸着跑了 15 分钟,没有记录跑了多少公里,我打算明天记录一下跑步的公里数。跑完步给手机充了一会儿就出门吃早餐,需要坐两站地铁才能到那家包子铺,我住的地方距地铁站大概 1.5 公里,这段路可以坐小区的摆渡车,但我选择了走路。


那家包子铺叫什么名儿?不记得了,更准确的说,我从来没想过要记住它的名字。我买了两个豇豆包,一个卤鸡蛋还有一杯豆浆。为什么要买这些食物呢?不是没有缘由。昨天我只买了两个豇豆包和一杯豆浆,没有吃饱,于是今天加了一个卤鸡蛋。今天买的这些食物,还是没让我吃饱,明天我要再加一个豇豆包。


为什么要买豇豆包呢?因为我喜欢吃酸豇豆,但是这包子里包的不是酸豇豆,而是新鲜的豇豆,所以第一次吃它的时候它与我的预期不符合,尝了之后又发现新鲜的豇豆也好吃,现在我每次都买豇豆包。今天我看见豇豆包里包的不是豇豆,而是四季豆,四季豆也挺好吃。


这家包子店里能喝的食物除了有豆浆还有稀饭,它的稀饭不是很稀,我担心豇豆包就着稀饭吃不好下咽,于是买了豆浆。


吃了早餐我就回家了,到家的第一件事是联系物业师傅来疏通地漏,还让他们处理电动晾衣架不能升降的问题。关于电动晾衣架,我今天学到了一个新知识,有些人可能会认为那是小常识,不值一提,但我还是要写下来——电池放在遥控器里久了会腐蚀铜板,这将导致遥控器通不了电,遥控器就指挥不了晾衣架,我家的晾衣机不能升降就是这个原因的。


今天我写了两篇技术文章去参加掘金推出的金石计划征文活动,这可以瓜分奖金,这是我第 5 次参加金石计划,每一次都瓜分到了最高奖金,这次我瓜分不到最高奖金了,因为写不出 6 篇原创技术文章。


等物业师傅处理好地漏和晾衣架,发布了文章,我就出发去健身房游泳,健身房离我家有 4.8 公里左右,出发前我在想是开车还是走路,最终选了走路。


今后要做什么?在路上我不由得思考。


今年是我工作的第 6 年,6 年来我断断续续的有考研的想法。为什么想考研呢?在路上我想出了一个原因 —— 源自虚荣心。走着走着想上厕所,路过一家花店,进店问老板附近是否有厕所,他说马路对面有,到了马路对面又向烟酒铺的老板询问厕所的具体位置,如愿的上了厕所,一身轻松。


上完厕所路过一家包子铺,肚子饿了,于是进店买了 2 个包子,1 个鸡蛋还有 1 碗粥,其实我想喝带丝汤,但是卖完了,所以没喝成。


吃完午餐继续往健身房走。哦,我想起在包子铺吃午餐的时候看到了一个皮肤特别好的女生。走呀走呀,终于到了健身房,到那儿之后我没有立即去游泳池,而是去休闲区按摩,在按摩椅上躺着的时候,开始思考我是否想继续当程序员,如果当程序员,想做那方面的业务呢?


我喜欢写作。2021 年期间我写了一部小说,超过 10 万个字,2022 年写我的第一本技术书,上个月才交稿,出版社编辑说稿子问题不大,昨天我根据编辑老师的反馈做了修改,并补充了前言,现在稿件已进入 3 审。我还有一个微信公众号,每个月至少发一篇技术文章。


如果继续当程序员,我想做内容创作类的产品。


按摩完就去游泳,游泳池里只有两个人,除了我还有一个小男孩,他挺可爱的。游完 3 圈我离开游泳池去了桑拿室,那儿只有我一个人,当时我突然想到我还想当程序员,不禁眼睛里有了点泪花。


我很爱哭,被裁后我一个人在家哭了好几次。哭,不是因为被裁,因为以前没被裁的时候,我一个人在家也有哭的时候。桑拿室温度很高,泪花流出来就蒸发了。蒸完桑拿洗完澡打算回家,可是外面在下雨,于是又到按摩椅上躺了一会儿,寻思着等雨停了再回家。第二次按完摩,雨没有停的迹象,我决定淋着雨到外面去打车,16 点到的家,在家吃了一包薯片,看了一会莫言的小说《生死疲劳》,17 点的时候到床上睡觉,18 点的时候才醒,然后到小区门口买了做晚餐的蔬菜,今天晚上煮了面。




这篇文章写了 2 个小时。最后我想说今天还有一件想做的事情没有做成——开车到兴隆湖吹风。


作者:何遇er
链接:https://juejin.cn/post/7270831230069243961
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

求你了,别再说不会JSONP了

JSONP是一种很远古用来解决跨域问题的技术,当然现在实际工作当中很少用到该技术了,但是很多同学在找工作面试过程中还是经常被问到,本文将带您深入了解JSONP的工作原理、使用场景及安全注意事项,让您轻松掌握JSONP。 JSONP是什么? JSONP,全称JS...
继续阅读 »

JSONP是一种很远古用来解决跨域问题的技术,当然现在实际工作当中很少用到该技术了,但是很多同学在找工作面试过程中还是经常被问到,本文将带您深入了解JSONP的工作原理、使用场景及安全注意事项,让您轻松掌握JSONP。


JSONP是什么?


JSONP,全称JSON with Padding,是一项用于在不同域之间进行数据交互的技术。这项技术的核心思想是通过在页面上动态创建<script>标签,从另一个域加载包含JSON数据的外部脚本文件,然后将数据包裹在一个函数调用中返回给客户端。JSONP不仅简单而且强大,尤其在处理跨域数据请求时表现出色。


JSONP的工作原理


JSONP的工作流程如下:


  • 客户端请求数据:首先,客户端会创建一个<script>标签,向包含JSON数据的远程服务器发出请求。这个请求通常包括一个名为callback的参数,用来指定在数据加载完毕后应该调用的JavaScript函数的名称。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSONP Example</title>
</head>
<body>
<h1>JSONP Example</h1>
<div id="result"></div>

<script>
// 定义JSONP回调函数
function callback(data) {
const resultDiv = document.getElementById('result');
resultDiv.innerHTML = `Name: ${data.name}, Age: ${data.age}`;
}

// 创建JSONP请求
const script = document.createElement('script');
script.src = 'http://localhost:3000/data?callback=callback';
document.body.appendChild(script);
</script>
</body>
</html>

  • 服务器响应:服务器收到请求后,将JSON数据包装在指定的回调函数中,并将其返回给客户端。响应的内容类似于:
const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

// 定义一个简单的JSON数据
const jsonData = {
name: 'John',
age: 30,
};

// 添加路由处理JSONP请求
router.get('/data', (ctx) => {
const callback = ctx.query.callback;
if (callback) {
ctx.body = `${callback}(${JSON.stringify(jsonData)})`;
} else {
ctx.body = jsonData;
}
});

// 将路由注册到Koa应用程序
app.use(router.routes()).use(router.allowedMethods());

// 启动Koa应用程序
const port = 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});


  • 客户端处理数据:在客户端的页面中,我们必须事先定义好名为callback的函数,以便在响应被加载和执行时被调用。这个函数会接收JSON数据,供我们在页面中使用。

JSONP使用场景


跨域请求:JSONP主要用于解决跨域请求问题,尤其适用于无法通过CORS或代理等方式实现跨域的情况。
数据共享:在多个域名之间共享数据,可以利用JSONP实现跨域数据共享。
第三方数据获取:当需要从第三方网站获取数据时,可以使用JSONP技术。


使用JSONP注意事项


JSONP的简单性和广泛的浏览器支持使其成为跨域数据交互的强大工具。然而,我们也必须谨慎使用它,因为它存在一些安全考虑,我们分析下它的优缺点:


优点


  • 简单易用:JSONP非常容易实现和使用,无需复杂的配置。
  • 跨浏览器支持:几乎所有现代浏览器都支持JSONP。
  • 绕过同源策略:JSONP帮助我们绕过了同源策略的限制,轻松获取跨域数据。

安全考虑


  • XSS风险:JSONP未经过滤的数据可能会引起XSS攻击,因此需要对返回的数据进行过滤和验证。
  • CSRF攻击:使用JSONP时要注意防范CSRF攻击,可以通过添加随机数等方式增强安全性。
  • 仅支持GET请求:JSONP只支持GET请求,不适用于POST等其他HTTP方法。
  • 难以处理HTTP错误:JSONP难以有效处理HTTP错误,在请求失败时的异常处理比较困难。

随着技术的发展,JSONP已不再是首选跨域解决方案,但了解它的工作原理仍然有助于我们更深入地理解跨域数据交互的基本原理。在实际项目中,根据具体需求和安全考虑,建议优先选择CORS或代理服务器方式处理跨域问题。


作者:小飞棍
链接:https://juejin.cn/post/7280435879548715067
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么日本的网站看起来如此不同

快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器,ChatGPT4 已上线 cube.waixingyun.cn/home 该篇文章讨论了日本网站外观...
继续阅读 »

快来免费体验ChatGpt plus版本的,我们出的钱 体验地址:chat.waixingyun.cn 可以加入网站底部技术群,一起找bug,另外新版作图神器,ChatGPT4 已上线 cube.waixingyun.cn/home


该篇文章讨论了日本网站外观与设计的独特之处。作者指出日本网站设计与西方设计存在明显差异。文章首先强调了日本网站的视觉风格,包括丰富的色彩、可爱的角色和复杂的排版。作者解释了这种风格背后的文化和历史因素,包括日本的印刷传统和动漫文化。


文章还讨论了日本网站的信息密集型布局,这种布局适应了日本语言的特点,使得页面能够容纳大量文字和图像。此外,文章提到了日本网站的功能丰富性,如弹出式窗口和互动元素,以及这些元素在用户体验方面的作用。


作者强调了日本网站在技术和创新方面的进步,尽管在过去存在技术限制。最后,文章提出了一些关于如何将日本网站设计的元素应用到其他文化中的建议。


下面是正文~~~


多年来,我朋友与日本的网站有过许多接触——无论是研究签证要求、计划旅行,还是简单地在线订购东西。而我花了很长时间才适应这些网站上的大段文字、大量使用鲜艳颜色和10多种不同字体的设计,这些网站就像是直接冲着你扔过来的。




虽然有许多网站都采用了更简约、易于导航的设计,适应了西方网站的用户,但是值得探究的是为什么这种更复杂的风格在日本仍然盛行。


只是为了明确起见,这些不是过去的遗迹,而是维护的网站,许多情况下,它们最后一次更新是在2023年。




我们可以从几个角度来分析这种设计方法:


  • 字体和前端网站开发限制
  • 技术发展与停滞
  • 机构数字素养(或其缺乏)
  • 文化影响

与大多数话题一样,很可能没有一个正确的答案,而是这个网站设计是随着时间的推移而相互作用的各种因素的结果。


字体和前端网站开发限制


对于会一些基本排版知识、掌握适当软件并有一些空闲时间的人来说,为罗马化语言创造新字体可能是一项有趣的挑战。然而,对于日语来说,这是一个完全不同层次的努力。


要从头开始创建英文字体,需要大约230个字形——字形是给定字母的单个表示(A a a算作3个字形)——或者如果想覆盖所有基于拉丁字母表的语言,则需要840个字形。对于日语而言,由于其三种不同的书写系统和无数的汉字,需要7,000至16,000个字形甚至更多。因此,在日语中创建新字体需要有组织的团队合作和比其拉丁字母表的同行们更多的时间。



这并不令人意外,因此中文和(汉字)韩文字体也面临着类似的工作量,这导致这些语言通常被称为CJK字体所覆盖。



由于越来越少的设计师面对这个特殊的挑战,建立网站时可供选择的字体也越来越少。再加上缺乏大写字母和使用日文字体会导致加载时间较长,因为需要引用更大的库,这就不得不采用其他方式来创建视觉层次。


以美国和日本版的星巴克主页为例:


美国的:




日本的




就这样,我们就可以解释为什么许多日本网站倾向于用文字较多的图片来表示内容类别了。有时,你甚至会看到每个磁贴都使用自己定制的字体,尤其是在限时优惠的情况下。




技术发展/停滞与机构数字素养


如果你对日本感兴趣,你可能对现代与过时技术之间的鲜明对比有所了解。在许多地方,先进的技术与完全过时的技术并存。作为世界机器人领导者之一的国家,在台场人工岛上放置了一座真人大小的高达雕像,却仍然依赖软盘和传真机,面对2022年Windows资源管理器关闭时感到恐慌。




在德国,前总理安格拉·默克尔在2013年称互联网为“未知领域”后,遭到全国范围的嘲笑。然而,这在2018年被前网络安全部长樱田义孝轻易地超越,他声称自己从未使用过电脑,并且在议会被问及USB驱动器的概念时,他被引述为“困惑不解”(来源)。


对于那些尚未有机会窥探幕后幻象的人来说,这可能听起来很奇怪,但日本在技术素养方面严重落后于更新计划。因此,可以推断这些问题也在阻碍日本网站设计的发展。而具体来说,日本的网页设计正面临着这一挑战——只需在谷歌或Pinterest上搜索日本海报设计,就能看到一个非常不同和现代化的平面设计水平。




文化影响


在分析任何设计选择时,不应低估文化习俗、倾向、偏见和偏好的影响。然而,“这是文化”的说法可能过于简单化,并被用作为各种差异辩解的借口。而且,摆脱自己的观点偏见是困难的,甚至可能无法完全实现。


因此,从我们的角度来看,看这个网站很容易..




感觉不知所措,认为设计糟糕,然后就此打住。因为谁会使用这个混乱不堪的网站呢?


这就是因为无知而导致有趣的见解被忽视的地方。现在,我没有资格告诉你日本文化如何影响了这种设计。然而,我很幸运能够从与日本本土人士的交谈中获得启发,以及在日本工作和生活的经验。


与这个分析相关的一次对话实际上不是关于网站,而是关于YouTube的缩略图 - 有时候它们也同样令人不知所措。




对于习惯了许多西方频道所采用的极简和时尚设计——只有一个标题、重复的色彩搭配和有限的字体——上面的缩略图确实有些难以接受。然而,当我询问一个日本本土人士为什么许多极受欢迎频道的缩略图都是这样设计时,他对这种设计被视为令人困惑的想法感到惊讶。他认为日本的设计方法使视频看起来更加引人入胜,提供了一些信息碎片,从而使我们更容易做出是否有趣的明智决策。相比之下,我给他看的英文视频缩略图在他看来非常模糊和无聊。


也许正是这种寻求信息的态度导致了我们的观念如此不同。在日本,对风险的回避、反复核对和对迅速做出决策的犹豫明显高于西方国家。这与更加集体主义的社会心态紧密相连——例如,在将文件发送给商业伙伴之前进行两次(或三次)检查可能需要更长时间,但错误的风险显著降低,从而避免了任何参与者丢面子的情况发生。


尽管有人认为这只适用于足够高的赌注,而迷惑外国游客似乎不符合条件——搜索一下“Engrish”这个词,然后感谢我吧。


回到网站设计,这种文化角度有助于解释为什么在线购物、新闻和政府网站在外部观察者看来常常是“最糟糕的罪犯”。毕竟,这些正是需要大量细节直接对应于做出良好购买决策、高效地保持最新信息或确保你拥有某个特定程序的所有必要信息的情况。


有趣的是,关于美国人和中国/日本人如何感知信息,也有相当多的研究。几项研究的结果似乎表明,例如,日本人更加整体地感知信息,而美国人倾向于选择一个焦点来引导他们的注意力(来源)。这可能给我们提供了另一个线索,解释为什么即使在日语能力较高的情况下,西方人对这类网站也感到困难。


后但并非最不重要的是,必须说的是,网站并不是在一个在线真空中存在。而且,各种媒体,从小册子或杂志到地铁广告,也使用了尽可能多地压缩信息的布局,人们可能已经习惯了这种无处不在的方式,以至于没有人曾经想过质疑它。


长话短说,这并不是为了找到标题问题的绝对答案,也不是为了加强日本人独特性的观点,就像日本人论一样。相反,尤其是在看到了几次关注一个解释为“真正答案”的讨论之后,我想展示科技、历史和文化影响的广度,这些最终塑造了这种差异。


作者:王大冶
链接:https://juejin.cn/post/7272290608655941651
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

《程序员的底层思维》:解密软件背后的16种底层思维能力

本文是关于《程序员底层思维》的阅读笔记,读后有较大收获,分享给大家。书中介绍了程序员应该具备的16中底层底层思维能力,只有意识到思维能力的存在,我们才有可能去学习、练习和提升。 基础思维能力 抽象思维 抽象思维是程序员最重要的思维能力之一,抽象的过程就是通过归...
继续阅读 »


本文是关于《程序员底层思维》的阅读笔记,读后有较大收获,分享给大家。书中介绍了程序员应该具备的16中底层底层思维能力,只有意识到思维能力的存在,我们才有可能去学习、练习和提升。


基础思维能力


抽象思维


抽象思维是程序员最重要的思维能力之一,抽象的过程就是通过归纳概括、分析综合来寻找共性、提炼相关概念的过程。


软件工程师每天都要动用抽象思维,首先对问题域进行分析、归纳、综合、判断、推理,然后抽象出各种概念,挖掘概念和概念之间的关系,再对问题域进行建模,最后通过编程语言实现业务功能。


抽象具有层次性,抽象层次越高,内涵越小,外延越大,扩展性越好;反之,抽象层次越低,内涵越大,外延越小,扩展性越差,但语义表达能力越强。


逻辑思维


逻辑思维基本包含3个方面的要素。(1)概念:概念是思维的基本单位。(2)判断(proposition,在逻辑学中也叫命题):通过概念对事物是否具有某种属性进行肯定或否定的回答,就是判断。(3)推理(argument,在逻辑学中也叫论证):由一个或几个判断推出另一判断的思维形式,就是推理。


判断是概念的展开,没有判断,就不能揭示和说明概念。同时,判断也是推理的前提,是正确运用各种推理的必要条件。所谓推理,就是研究语句、判断、命题之间相互关系的学问。建模是一个归纳工作,我们通过抽象问题域里具有共同特性的类来建立模型。为了验证模型的有效性,我们会使用演绎的方法去推演不同的业务场景,看看模型是否能满足业务的需要。


大多数情况下,我们的思维逻辑链都比较短,短就意味着肤浅,找不到问题的根本原因。延长思维逻辑链的方法之一是5Why思考法,它能够帮助我们找到问题的根本原因。5Why思考法,是指对一个问题连续多次追问为什么,直到找出问题的根本原因。


丰田汽车公司前副社长大野耐一曾经举了一个通过5Why提问法找到问题根本原因的实例。有一次,大野耐一先生见到生产线上的机器总是停转,虽然修过多次但仍不见好转,便上前询问现场的工作人员。



问:“为什么机器停了?”(1Why)


答:“因为机器超载,保险丝烧断了。”


问:“为什么机器会超载?”(2Why)


答:“因为轴承的润滑不足。”


问:“为什么轴承会润滑不足?”(3Why)


答:“因为润滑泵吸不上来油。”


问:“为什么润滑泵吸不上来油?”(4Why)


答:“因为油泵轴磨损、松动了。”


问:“为什么油泵轴磨损了?”(5Why)


答:“因为没有安装过滤器,润滑油里混进了铁屑等杂质。”



结构化思维


结构化思维是一种以逻辑(事物内在规律)为基础,从无序到有序搭建结构的思维过程,其目的是降低复杂度和认知成本,因为大脑更喜欢概念少、有规律的信息。


结构是万物之本,小到分子,大到宇宙,只有了解其结构才能真正认识它。


批判性思维


批判性思维中的“批判”一词其实不太准确,甚至在中文里有点否定、批评和抨击的负面意思。批判的英文是critical,这个词来自古希腊词kriticos,是分辨力、决断力或决策能力的意思。它强调的是理性和逻辑在思维中的重要性,目的是形成正确的结论,并做出明智的决策和判断。所以批判性思维并不是让你批评、否定或者抨击别人,而是教你如何提升分辨能力、判断力。


简而言之,批判性思维就是对思维过程的再思考。古希腊哲学家苏格拉底说,未经审视的人生不值得过。同样,未经批判性思维审视过的结论也是不值得相信的。


维度思维


一个人的思维层级与其思考的维度是正相关的,这一点可以通过我们的日常语言得到佐证。当我们说这个人很“轴”“一根筋”的时候,实际上是在说他只有一维的线性思维;高手的思考会更加“全面”,因为涉及“面”,所以至少是两个维度的思考;而真正的高手,其思考是成“体系化”的,“体”至少是三维的,也就是说他考虑到了“方方面面”。


多维度思考是思考的高级阶段,是体系化思考的必备,是解决复杂问题的一把利器。


分类思维


当信息量过大时,归类分组能帮助我们理解和处理问题。分类是人类大脑的识别模式,是我们化繁为简的不二法宝。在我们处理问题,特别是复杂问题的时候,分类思维扮演了极其重要的角色。


分类的目的是找到问题域中的“核心抽象”,基于这些“核心抽象”,我们才能设计相应的领域模型和数据模型;基于这些模型,我们才能构建相应的系统。


没有完美的分类,任何分类都与进行分类的观察者的视角和目的有关。


分治思维


分治的价值在于,我们不应该试着在同一时间把整个问题域都塞进自己的大脑,而应该试着以某种方式去组织问题,以便能够在一个时刻专注于一个特定的部分。


分治算法主要包含3个步骤:分、治、并。“分”是递归地将原问题分解成小问题;“治”是在解决了各个小问题之后(各个击破之后)合并小问题的解,从而得到整个问题的解;“并”是按原问题的要求,将子问题的解逐层合并,构成原问题的解。


软件中存在大量的分治思想,比如管道模式、分层架构、分布式架构等,无不体现了分治的强大。


简单思维


把一件事情搞复杂是一件简单的事,但要把一件复杂的事变简单,这是一件复杂的事。


简化本质上是一个熵减活动。所有的事物都在缓慢熵增,就像凯文·凯利在《必然》一书中提到,世间万物都需要额外的能量和秩序来维持自身,无一例外。这就是著名的热力学第二定律,即所有的事物都在缓慢地分崩离析。而熵减就是逆向做功,即通过更多的努力让混乱的系统重新归于秩序。


简单不是一个简单的目标,而是一个非常高的目标。所有的UNIX哲学浓缩为一条铁律就是KISS原则。


简单不是简陋。简单是一种洞察问题本质、化繁为简的能力,简陋是对问题不加思考地简单处理,二者有本质区别。简单需要我们付出很多的精力,对问题深入思考,进行熵减逆向做功。往往需要经历简单—复杂—简单的演化过程。


成长型思维


想要培养自己的成长型思维,首先要学会正确评价自己,也就是要学会客观地看待自己的状况和水平,不要自视过高,也不要妄自菲薄。其次,不要过分相信天分。一个人一旦相信了天分,就等于相信了自己的水平是基本不变的,就会给自己设限,觉得我只能做这个,我不适合干那个,甚至会觉得努力是一件丢脸的事情,只有笨人才需要努力。


看过《刻意练习:如何从新手到大师》一书的人应该知道,很多所谓的天才,其实靠的并不是天分,而是努力!想要让自己获得成长和改变,就一定要学会用成长型思维去看待和处理问题,其关键在于不要自我设限。


专业思维能力


解耦思维


在软件领域,“耦合”是指两个事物之间联系的紧密程度。联系越紧密,耦合性越高;联系越少,耦合性越低。解耦就是要减少事物之间联系的紧密程度。


“计算机中的任何问题,都可以通过加一层来解决”,中间层的价值也在于解耦。


“高内聚、低耦合”是软件设计追求的重要目标之一,组件、模块、层次设计都应该遵循“高内聚、低耦合”的设计原则。


应用架构之道,就是要实现业务逻辑和技术细节的解耦。


契约思维


“人是生而自由的,但却无往不在枷锁之中”,同样,“写代码是自由的,但无往不在规则之下”。这里的规则包括工程师必须要遵守的程序语言语法、编程规范,以及协议标准。


为了保证软件编程风格的一致性,减少随心所欲带来的复杂度,我们有必要使用契约思维制定一定程度上的编程规范,去约束团队的行为。规范的价值,就在于它能保证代码的一致性,而一致性在很大程度上可以降低认知成本和复杂度。


通过在团队中落实命名规范、异常处理规范、架构规范等,可以有效地帮助团队治理代码复杂度。


社会大规模分工协作离不开契约思维,编程在很大程度上是一种“制定契约”。


模型思维


在软件工程中,有两个高阶工作,一个是架构,另一个是建模。如果把写代码比喻成“搬砖”,那么架构和建模就是“设计图纸”了。相比于编码,建模的确是对设计经验和抽象能力要求更高的一种技能。


简单来说,模型就是对现实的简化抽象。


领域模型将现实世界抽象为了信息世界,把现实世界中的客观对象抽象为某一种信息结构,而这种信息结构并不依赖于具体的计算机系统。


领域模型对软件开发至关重要。因为从本质上来说,软件开发就是从问题空间到解决方案空间的映射转化,而领域模型是连接问题和解决方案的桥梁。


领域模型关注的是领域知识,是业务领域的核心实体,体现了问题域中的关键概念,以及概念之间的联系。领域模型建模的关键在于模型能否显性化、清晰地表达业务语义,其次才是扩展性。


数据模型关注的是数据存储,所有的业务都离不开数据,以及对数据的CRUD。数据模型建模的决策因素主要是扩展性、性能等非功能属性,无须过多考虑业务语义的表征能力。


工具化思维


我们可以把“懒”分为3个境界。(1)最低境界是“实在懒”,拖延症,不到万不得已,不去完成任务。(2)其次是“开明懒”,迅速做完不喜欢的任务,以摆脱之。(3)最高境界是“智慧懒”,使用工具完成不喜欢的任务,以便再也不用做无谓的重复工作,从而一劳永逸。


量化思维


No measurement,no improvement.(没有量化,就无法优化。)——“科学管理之父”温斯洛·泰勒


一个量化的过程大体上可以分为以下3步。(1)定义指标:仔细分析问题,找到那个可以用来量化问题的关键指标。(2)将指标数字化:围绕关键指标,明确需要哪些数据来实现指标的计算,通过数据收集、数据存储、数据展现去呈现指标,也就是数字化的过程。(3)优化指标:有了数据指标之后,要围绕指标数据迭代优化,达成业务目标。


量化工作本身是一件非常困难和极具挑战的事情,但量化思维要求我们不要轻易放弃关于量化的思考和尝试。没有量化的目标,就像是断了线的风筝,没有方向,缺少指引,飞到哪里是哪里,而量化后的目标可以为我们清楚地指引方向。


数据思维


一切业务数据化,一切数据业务化。


用户在App上的每一次浏览、每一次点击、每一次搜索等业务行为,都会被沉淀为数据保存起来,这种保存业务过程数据的做法叫作业务数据化。这些数据会帮助App更好地认识用户,当用户下次打开App时,利用这些数据,App就可以更精准地为用户进行智能推荐和广告精准投放,这种用数据赋能业务的方法叫作数据业务化。


产品思维


工程思维和产品思维是不一样的。工程师追求技术至上,产品经理追求商业价值和用户体验;工程师关注细节,产品经理关注全局;工程师关注How(如何做),产品经理关注Why(为什么)。结合两种思维方式,可以让思考更全面和系统化。


作为技术人员,我们必须要具备一定的产品思维,这样才能辨别产品需求的真伪,把伪需求挡在外面,从而可以把时间放在真正有价值的项目上,少做一些无效的投入。对于团队的技术负责人来说,这种把关尤为重要。


了解产品思维,关键要理解产品的三个核心要素:用户、需求、场景。(1)用户是产品要服务的对象,即使用产品的人。(2)需求即产品要解决的核心问题是什么。需要注意的是,需求是分层次的,最浅一层是需求的表象;第二层是观点和背后的目的;最深一层是人性,每个需求挖到最后,都可以归结到人性层面。(3)场景即用户何时何地需要使用产品。


后记


希望读者能够把这些底层思维能力内化成自己的“不知道自己知道”。这些底层思维中蕴藏着解决问题的强大力量,当它们与软件设计相遇时,会擦出耀眼的“火花”。


软件开发行业的匠心和传统行业的匠心不一样,不是重复做简单的事情,你就能把它做好。这就好比你即使做了10年的收银员,也只是一个收银员,无法成为财务总监。在软件开发行业,你需要不断地学习、不断地思考、不断地积累、不断地尝试、不断地失败、不断地创新,才有可能做得好。


优秀的工程师,心中都有一团火——一种对美的追求和渴望。这需要我们经历无数个不眠之夜,承受很大的压力,受很多委屈,看很多的书,尝试很多别人没有实践过的东西,要具有一颗“不妥协、不将就、不放弃”的倔强的心。这样我们才能做出一些不同凡响的东西,才能活成自己所期望的样子。


作者:楚兴
来源:juejin.cn/post/7277830857422602252
收起阅读 »

如何努力才能成为核心骨干?

我相信每个人都想成为组里的核心骨干,不用打酱油,不用干杂活,可以选择最有挑战的工作、最有收益的工作,可以把杂活脏活累活甩给其他人。 有更多的锻炼机会,有更好的晋升机会…… 但组里的核心骨干毕竟是少数,就像年终绩效考评S和 A 绩效总是极少数人。大多数人只能拿 ...
继续阅读 »

我相信每个人都想成为组里的核心骨干,不用打酱油,不用干杂活,可以选择最有挑战的工作、最有收益的工作,可以把杂活脏活累活甩给其他人。 有更多的锻炼机会,有更好的晋升机会……


但组里的核心骨干毕竟是少数,就像年终绩效考评S和 A 绩效总是极少数人。大多数人只能拿 B。


我的三份工作都把前辈熬走了


熬走了前辈,我就成为了核心骨干。


在我待过的三家公司来看,第一家是新创项目,我就是元老,虽然刚毕业,但被安排负责了一部分核心工作,然而另一个名校应届生因为来的晚,只能负责监控打点优化超时的一些杂活,我则始终参与核心业务需求和核心框架升级。我觉得我的能力不比他强,只是我来的早一点罢了。


第二份工作,我入职时候表现很差。因为公司不加班,我表现的过于放飞自我,下班较早,被老板认为摆烂没上进心。一直被安排负责边缘业务。但是待了一年半不到,比我入职早的、同时入职的都离职跑路了,看得出领导是捏着鼻子把我当核心骨干用。后来换了领导,新的领导更是把我当骨干用。就这样新的一年我接触核心业务,核心技术改造,进步确实比之前干杂活快的多。


第三份工作是在美团外卖,比较核心的业务,但是一开始也是负责杂活。老员工把他一些不想干的小技术优化分给我,还说"这些容易出成果","呵呵"。这些小活杂活真的干起来很没有意思。但是互联网人员流动快的铁律又出现了,才入职一年,负责核心模块的两个员工相继离职,而我毫无疑问,马上接替他们的工作。之前虽然给他俩打杂,但好歹我平时没少熟悉代码,熟悉业务逻辑,所以最适合接替他俩的自然是我。


总结下来,这三份工作我从打杂到核心骨干,并不是靠卷出来的。靠内卷上位,真的是非常难的。大家都是一个脑袋,凭什么你更强。大家都是 24 小时,凭什么你更强。你作为后来的,很难挤掉其他人上位。


当核心骨干要靠【 熬 】


成核心骨干是适合大多数人的方式。新同学进入到新的环境,想要超越组里的老人,是一件很有挑战的事情。因为大多数工作只要做的久就一定能胜任,系统再复杂再有挑战,组里总会有人熟悉。因为系统代码都是他们写的,你作为一个新人 想追上再超越非常困难。(想要超车,必须要有弯道啊)


就算你工作能力突出,核心工作就会交给你负责吗?不然。组里的老员工负责核心工作干的好好地,为什么主管会让你负责呢?让你负责核心工作,组里的核心老员工去干啥,干杂活吗?从个人感情上,老员工更亲。从工作经验上老员工更多。新人要想超越,需要付出太多。


就我个人经验而言,组里每个人都负责其中一部分工作,在没有特殊情况下,不会出现工作内容的交换。(不排除个别团队会轮换工作内容)


把老员工熬走,你就是老员工。把核心员工熬走,你就是核心员工。”熬“是一种平和的心态,让我们戒骄戒躁,当被安排一些杂活时,能保持平静。只有情绪平和,才能好好地工作。


互联网公司人员流动快,虽说是熬成老员工。但可能只需要熬一年就成老员工。


熬是一种良好心态,让我们脚踏实地,眼望天空


熬着,并不是把上班当煎熬。而是能摆正心态,认识到被分配干杂活是正常的,并不是针对你,每个人都是这么过来的。 熬着是一种耐心,相信早晚有一天轮到自己。


平时要把每一份交给自己的工作做好。否则,等核心员工离职了,领导也不信任你,还继续让你干杂活,这就真是悲剧了。


劳资要和他们斗到底


工作中难免有不顺心的时候,可能是产品经理奇葩需求太多,加班太多。也可能是和上下游团队吵架甩锅。甚至可能是间歇性疲倦。


不顺心,难免会想着离职跳槽,跳槽也许还能涨薪,诱惑很大。


但是成为核心骨干后,做最有挑战的工作才能让人成长最快。如果轻易跳槽,就错失最好最快的成长机会。


当面对工作中的不顺心,要始终保持 一份斗争的心态。告诉自己,谁欺负自己,就和他斗到底。



  1. 产品奇葩需求多,就和他争排期,争需求的不合理,争取领导协调更多人力。

  2. 上下游团队吵架甩锅。就和大家学,如何微笑的吵架,微笑的把锅甩出去。

  3. 加班多,周末就多出去放松、锻炼下身体、吃点好吃的。让自己开心。同时也要向领导寻求帮助,或者传递自己加班太多的委屈。不要让自己的辛苦被领导忽视。


“劳资要和他们斗到底” 这是对”敌人“的宣战,更是对一个懦弱、爱放弃、爱退缩的自己宣战!


好事多磨,机会是熬出来的,优秀的人也是熬出来的。不要轻易跳槽哦~


大家一起共勉。


作者:他是程序员
来源:juejin.cn/post/7281499704220106815
收起阅读 »

父母在家千万注意别打开“共享屏幕”,银行卡里的钱一秒被转走......

打开屏幕共享,差点直接被转账 今天和爸妈聊天端午回家的事情,突然说到最近AI诈骗的事情,千叮咛万嘱咐说要对方说方言才行,让他们充分了解一下现在骗子诈骗的手段,顺便也找了一下骗子还有什么其他的手段,打算一起和他们科普一下,结果就发现下面这一则新闻: 在辽宁大连务...
继续阅读 »

打开屏幕共享,差点直接被转账


今天和爸妈聊天端午回家的事情,突然说到最近AI诈骗的事情,千叮咛万嘱咐说要对方说方言才行,让他们充分了解一下现在骗子诈骗的手段,顺便也找了一下骗子还有什么其他的手段,打算一起和他们科普一下,结果就发现下面这一则新闻:


在辽宁大连务工的耿女士接到一名自称“大连市公安局民警”的电话,称其涉嫌广州一起诈骗案件,让她跟广州警方对接。耿女士在加上所谓的“广州警官”的微信后,这位“警官”便给耿女士发了“通缉令”,并要求耿女士配合调查,否则将给予“强制措施”。随后,对方与耿女士视频,称因办案需要,要求耿女士提供“保证金”,并将所有存款都集中到一张银行卡上,再把钱转到“安全账户”。


图片


期间,通过 “屏幕共享”,对方掌握了耿女士银行卡的账号和密码。耿女士先后跑到多家银行,取出现金,将钱全部存到了一张银行卡上。正当她打算按照对方指示,进行下一步转账时,被民警及时赶到劝阻。在得知耿女士泄露了银行卡号和密码后,银行工作人员立即帮助耿女士修改了密码,幸运的是,银行卡的近6万元钱没有受到损失。


就这手段,我家里的老人根本无法预防,除非把手机从他们手里拿掉,与世隔绝还差不多,所以还是做APP的各大厂商努力一下吧!


希望各大厂商都能看看下面这个防劫持SDK,让出门在外打工的我们安心一点。


防劫持SDK


一、简介


防劫持SDK是具备防劫持兼防截屏功能的SDK,可有效防范恶意程序对应用进行界面劫持与截屏的恶意行为。


二、iOS版本


2.1 环境要求


条目说明
兼容平台iOS 8.0+
开发环境XCode 4.0 +
CPU架构armv7, arm64, i386, x86_64
SDK依赖libz, libresolv, libc++

2.2 SDK接入


2.2.1 DxAntiHijack获取

官网下载SDK获取,下面是SDK的目录结构


1.png


DXhijack_xxx_xxx_xxx_debug.zip 防劫持debug 授权集成库 DXhijack_xxx_xxx_xxx_release.zip 防劫持release 授权集成库




  • 解压DXhijack_xxx_xxx_xxx_xxx.zip 文件,得到以下文件




    • DXhijack 文件夹



      • DXhijack.a 已授权静态库

      • Header/DXhijack.h 头文件

      • dx_auth_license.description 授权描述文件

      • DXhijackiOS.framework 已授权framework 集成库






2.2.2 将SDK接入XCode

2.2.2.1 导入静态库及头文件

将SDK目录(包含静态库及其头文件)直接拖入工程目录中,或者右击总文件夹添加文件。 或者 将DXhijackiOS.framework 拖进framework存放目录


2.2.2.2 添加其他依赖库

在项目中添加 libc++.tbd 库,选择Target -> Build Phases,在Link Binary With Libraries里点击加号,添加libc++.tbd


2.2.2.3 添加Linking配置

在项目中添加Linking配置,选择Target -> Build Settings,在Other Linker Flags里添加-ObjC配置


2.3 DxAntiHijack使用


2.3.1 方法及参数说明

@interface DXhijack : NSObject

+(void)addFuzzy; //后台模糊效果
+(void)removeFuzzy;//后台移除模糊效果
@end

2.3.2 使用示例

在对应的AppDelegate.m 文件中头部插入


#import "DXhijack.h"

//在AppDelegate.m 文件中applicationWillResignActive 方法调用增加
- (void)applicationWillResignActive:(UIApplication *)application {
[DXhijack addFuzzy];
}

//在AppDelegate.m 文件中applicationDidBecomeActive 方法调用移除
- (void)applicationDidBecomeActive:(UIApplication *)application {
[DXhijack removeFuzzy];
}


三、Android版本


3.1 环境要求


条目说明
开发目标Android 4.0+
开发环境Android Studio 3.0.1 或者 Eclipse + ADT
CPU架构ARM 或者 x86
SDK三方依赖

3.2 SDK接入


3.2.1 SDK获取


  1. 访问官网,注册账号

  2. 登录控制台,访问“全流程端防控->安全键盘SDK”模块

  3. 新增App,填写相关信息

  4. 下载对应平台SDK


3.2.2 SDK文件结构



  • SDK目录结构 android-dx-hijack-sdk.png



    • dx-anti-hijack-${version}.jar Android jar包

    • armeabiarmeabi-v7aarm64-v8ax86 4个abi平台的动态库文件




3.2.3 Android Studio 集成

点击下载Demo


3.2.3.1 Android Studio导入jar, so

把dx-anti-hijack-x.x.x.jar, so文件放到相应模块的libs目录下


android-dx-hijack-as.png



  • 在该Module的build.gradle中如下配置:


 android{
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}

repositories{
flatDir{
dirs 'libs'
}
}
}


dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}



3.2.3.2 权限声明

Android 5.0(不包含5.0)以下需要在项目AndroidManifest.xml文件中添加下列权限配置:


<uses-permission android:name="android.permission.GET_TASKS"/>

3.2.3.3 混淆配置

-dontwarn *.com.dingxiang.mobile.**
-dontwarn *.com.mobile.strenc.**
-keep class com.security.inner.**{*;}
-keep class *.com.dingxiang.mobile.**{*;}
-keep class *.com.mobile.strenc.**{*;}
-keep class com.dingxiang.mobile.antihijack.** {*;}

3.3 DxAntiHijack 类使用


3.3.1 方法及参数说明

3.3.1.1 初始化


建议在Application的onCreate下調用


/**
* 使用API前必須先初始化
* @param context
*/

public static void init(Context context);

3.3.1.2 反截屏功能


/**
* 反截屏功能
* @param activity
*/

public static void DGCAntiHijack.antiScreen(Activity activity);

/**
* 反截屏功能
* @param dialog
*/

public static void DGCAntiHijack.antiScreen(Dialog dialog);

3.3.1.3 反劫持检测


/**
* 调用防劫持检测,通常现在activity的onPause和onStop调用
* @return 是否存在被劫持风险
*/

public static boolean DGCAntiHijack.antiHijacking();

3.3.2 使用示例

//使用反劫持方法
@Override
protected void onPause() {
boolean safe = DXAntiHijack.antiHijacking();
if(!safe){
Toast.makeText(getApplicationContext(), "App has entered the background", Toast.LENGTH_LONG).show();
}
super.onPause();
}

@Override
protected void onStop() {
boolean safe = DXAntiHijack.antiHijacking();
if(!safe){
Toast.makeText(getApplicationContext(), "App has entered the background", Toast.LENGTH_LONG).show();
}
super.onStop();
}



//使用反截屏方法
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DXAntiHijack.antiScreen(MainActivity.this);
}

以上。


结语


这种事情层出不穷,真的不是吾等普通民众能解决的,最好有从上至下的政策让相应的厂商(尤其是银行和会议类的APP)统一做处理,这样我们在外打工的人才能安心呀。


作者:昀和
来源:juejin.cn/post/7242145254057312311
收起阅读 »

我裸辞了,但是没走成!

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了! 1.为什么突然想不干了? 1.奇葩的新组长 我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑! 今年过年前,...
继续阅读 »

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了!


1.为什么突然想不干了?


1.奇葩的新组长


我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑!


今年过年前,我主开发的平台要嵌入到他负责的项目里面,一切对接都很顺利,然而某天,有bug,我修复了,在群里面发消息让他合并分支更新一下。他可能没看到,然后我下班后一个小时半,我还在公司,在群里问有没有问题,没回应!


然后我就坐车回家,半路,产品经理组长、大组长和前组长一个个轮流call我,让我处理一下bug,我就很无语!然后我就荣获在家远程办公,发现根本没问题!然后发现是对方没更新的问题!后面我修复完直接私聊他merge分支更新,以免又这样大晚上烦人!
而类似的事情接连发生,第三次之后,我忍不住了,直接微信怼了他,他还委屈自己晚上辛苦加班,我就无语大晚有几个人用,晚上更新与第二天早上更新有什么区别?然后就这样彻底闹掰了!


我就觉得这人很奇葩,有什么问题不能直接跟我沟通,一定要找我的上级一个个间接联系我呢?而且,这更新流程就很有问题,我之前在别的组支援修bug,是大早上发布更新,一整天测试,保证不是晚上的时候出现要紧急处理的问题!


然后,我跟这人有矛盾后,我就没继续对接这个项目了,前组长安排了别人代替我!


结果兜兜转转,竟然调到他这里来!作孽啊!


2.项目组乱糟糟


新项目组可以看出新组长管理水平很糟糕!


新组长给自己的定位是什么都管!产品、前后端、测试、业务等,什么都往自己身上揽!他自己觉得很努力,但他不是那部分的专业人员,并不擅长,偏偏还没那个金刚钻揽一堆瓷器活!老爱提建议!


项目组就两个产品,其中一个是UI设计刚转,还没成长为专业的产品经理,而那个主要负责的产品经理根本忙不过来!


然后,他一个人搞不定,就开始了PUA大法,周会的时候就会说:“希望大家要把这个项目当成自己的事业来奋斗,一起想,更好地做这个产品!”


“这个项目集成了那么多的模块功能,如果大家能够做好,对自己有很大的历练和成长!”


“我们项目是团队的重点项目,好多领导都看好,开发不要仅限于开发,要锻炼产品思维……”


……


简而言之就是,除了本职工作也要搞点产品的工作!


然后建模师开始写市场调研文档,前后端开发除了要敲代码,还得疯狂想新功能。


整个组开始陷入搞新东西的奇怪旋涡!


某次需求评审的时候,因为涉及到大量的文件存储,我提出建议,使用minio,fastdfs,这样就不用每次部署的时候,整体文件还要迁移,结果对方一口拒绝,坚决使用本地存储,说什么不要用XX平台的思想来污染他这个项目,他这个项目就要不需要任何中间件都能部署。


就很无语!那个部署包又大又冗余,微服务都不用,必须要整包部署整套系统,只想要某几个功能模块都不行,还坚持说这样可以快速整包部署比较好!


一直搞新功能的问题就是版本更新频繁!一堆新功能都没测清楚就发布,导致产品质量出现严重问题,用户体验极差!终于用户积攒怨气爆发了,在使用群里面@了我们领导,产品质量问题终于被彻底揭开了遮羞布!


领导开始重视这个产品质量的问题,要求立即整改!


然后这个新组长开始新一轮搞事!


“大家保证新功能进度的同时,顺便测试旧功能,尽量不要出bug!”


意思就是你开发进度也要赶,测试也要搞!


就不能来点人间发言吗?


3.工作压力剧增


前组长是前端的,他带我入门3D可视化的,相处还算融洽!然而他辞职了,去当自由职业者了!


新组长是后端的,后端组长问题就是习惯以后端思维评估前端工作,给任务时间很紧。时间紧就算了,还要求多!


因为我之前主开发的项目是可视化平台,对方不太懂,但不妨碍他喜欢异想天开,加个这个,加个那个,他说一句话,就让你自行想象,研究竞品和评估开发时间!没人没资源,空手套白狼,我当时就很想爆他脑袋了!


我花一个星期集成了可视化平台的SDK,连接入文档都写好了,然后他验收的时候提出一堆动态配置的要求,那么大的可视化平台,他根本没考虑项目功能模块关联性和同步异步操作的问题,他只会提出问题,让你想解决方案!


然后上个月让我弄个web版的Excel表格,我看了好多开源项目,也尝试二开,增加几个功能,但效率真的好低!于是我就决定自己开发一个!


我开发了两个星期,他就问我搞定没?我说基本功能可以,复杂功能还在做!


更搞笑的是,我都开发两个星期了,对方突然中午吃饭的时候微信我,怕进度赶不上,建议我还是用开源的进行二开,因为开源有别人帮忙一起搞。


我就很无语,人家搞的功能又不是一定符合你的需求,开源不等于别人给你干活,大家都是各干各的,自己还得花精力查看别人代码,等价于没事找事,给自己增加工作量!别人开发的有隐藏问题,出现bug排查也很难搞,而自己开发的,代码熟悉,即便有问题也能及时处理!


我就说他要是觉得进度赶不上就派个人来帮忙,结果他说要我写方案文档,得到领导许可才能给人。


又要开发赶进度,又要写文档,哪有那么多时间!最终结果就是没资源,没人手,进度依旧要赶!


因为我主开发的那个可视化平台在公司里有点小名气,好多平台想要嵌入,然后,有别的平台找到他要加上这个可视化平台,但问题是我很忙,又要维护又要开发,哪搞得了那么多?还说这个很赶!赶你个头!明知道时间没有,就别答应啊!工作排期啊!


新组长不帮组员解决问题,反而把问题抛给组员,压榨组员就很让人反感!


2.思考逃离


基于以上种种!我觉得这里不是一个长久之地,于是想要逃离这里!


我联系了认识的其他团队的人,别人表示只要领导愿意放人,他们愿意接收我,然后我去咨询一些转团队的流程,那些转团队成功的同事告诉我,转团队最难的是领导放人这关,而且因为今年公司限制招聘,人手短缺,之前有人提出申请,被拒绝了!并且转团队的交接的一两个月内难免要承受一些脸色!有点麻烦!


我思虑再三,我放弃了转团队这条路,因为前组长走了之后,整个团队只剩下我一个搞3D开发的,估计走不掉!


3.提出辞职


忍了两个月,还是没忍住,工作最重要的是开开心心!赚钱是一回事,要是憋出个心理疾病就是大事了!于是我为了自己的身心健康,决定走人!拜拜了喂!老娘不奉陪了!


周一一大早,我就提交了辞职信,大组长表示很震惊,然后下午的时候,领导和大组长一起来跟我谈话,聊聊我为什么离职?问我有没有意愿当个组长之类的,我拒绝了,我只想好好搞技术!当然我不会那么笨去说别人的坏话得罪人!


我拿前组长当挡箭牌,说自己特别不习惯这个新组长的管理方式!前组长帮我扛着沟通之类的问题,我只要专心搞开发就好了!


最终,我意志坚定地挡住了领导和大组长的劝留谈话,并且开始刷面试题,投简历准备寻找新东家!


裸辞后真的很爽,很多同事得知消息都来关心我什么情况,我心里挺感动的!有人说我太冲动了,可以找好下家再走!但我其实想得很清楚,我没可能要求一个组长委屈自己来适应我,他有他的管理方式,我有我的骄傲!我不喜欢麻烦的事,更不喜欢委屈自己,一个月后走人是最快解决的方案!


4. 转机


其实我的离开带来了一点影响,然后加上新组长那个产品质量问题警醒了领导,然后新组长被调去负责别的项目了,换个人来负责现在的项目组,而这个人就是我之前支援过的项目组组长,挺熟悉的!


新新组长管理项目很有条理也很靠谱,之前支援的项目已经处于稳定运行的状态了,于是让他来接手这个项目!他特意找我谈话,劝我留下来,并且承诺以后我专心搞技术,他负责拖住领导催进度等问题!


我本来主要就是因为新组长的问题才走人的,现在换了个不错的组长!可以啊!还能苟苟!


5.反思



  1. 其实整件事情中,我也有错,因为跟对方闹掰了,就拒绝沟通,所以导致很多问题的发生,如果我主动沟通去说明开发难度的问题,并且争取时间,就不至于让自己处于一个精神内耗的不快乐状态。

  2. 发现问题后,我没有尝试去跟大组长反馈,让大组长去治治对方,或者让大组长帮忙处理这个矛盾,我真的太蠢了!

  3. 我性格其实挺暴躁的,看不顺眼就直接怼,讨厌的人就懒得搭理,这样的为人处世挺不讨喜的,得改改这坏脾气!


作者:敲敲敲敲暴你脑袋
来源:juejin.cn/post/7241884241616076858
收起阅读 »

怎么用一句话证明你在游戏公司里的最底层?

引言 今天在知乎看到一个有趣的帖子:如何一句话证明你在公司最底层?我们把范围缩小到游戏公司。 关于这个问题,身边80%的朋友描述了自己在公司底层的难忘回忆,还有几位朋友甚至因为这不堪的回忆破防了。 刚进入游戏公司的新人,迷茫是常态。和大家一样,笔者也曾是公司的...
继续阅读 »

如何一句话证明你在公司最底层?


引言


今天在知乎看到一个有趣的帖子:如何一句话证明你在公司最底层?我们把范围缩小到游戏公司。


关于这个问题,身边80%的朋友描述了自己在公司底层的难忘回忆,还有几位朋友甚至因为这不堪的回忆破防了。


刚进入游戏公司的新人,迷茫是常态。和大家一样,笔者也曾是公司的最底层,总觉得每天一睁眼就是各种困难的事等着我:



担心工作内容不会做,担心与同事沟通不好,担心自己考核不过关......



今天的这篇文章,大家一起来看看一位位于游戏公司底层的游戏开发者的最底层体验。


最底层体验


图片源于网络


1.介绍一下你自己


大家好,我是XXX,来自XXX。虽然我是一个新人,但我对游戏充满了热情,这种热情已经伴随我多年。小时候,我就沉迷于各种游戏,从那时起,我就梦想着有一天能够为创造令人陶醉的游戏世界做出贡献。我加入这个行业的目标是成为一个出色的游戏开发者,并参与创造令人惊叹的游戏体验。我相信,通过与这个行业的优秀人才一起工作,我可以不断成长,并为我们的团队和项目做出贡献。谢谢大家,请多多指教。


此处应有一阵热烈的掌声,那是对一位懵懂的游戏行业新人的勇敢表示敬畏。他或许不知道他的棱角将在这里被磨平。


熟悉又让人崩溃的弹窗


2.熟悉项目,体验游戏。


游戏行业新人刚进到游戏公司,可能第一件事就是登陆公司内部使用的通讯工具。你的直属上司可能早早的在网线那头等候着你的上线。


你好,XXX。你先接收一下这份文档,仔细阅读一下里面的内容。检出一下公司的游戏项目,然后根据文档把游戏跑起来。体验一下游戏,熟悉一下游戏的每个系统。有问题可以请教你旁边的那位大神,他负责带你。


好的,谢谢。由于在来公司之前做足了准备,检出项目、运行项目这种小问题肯定难不倒你。这时候你会惊讶,原来这就是大型的商业化游戏项目,看起来有那么点高大上,但是最多的是还是看不懂。不过这游戏玩着好无聊,不是我喜欢的类型。想到未来的日子里,需要不停地重复地在这个游戏里面遨游,"真的会谢"。



3.分配任务



  • 修改禅道bug序号XXX的问题。

  • 修改活动XXX文本显示异常问题。

  • 修改XXX报错问题,完成禅道单子序号1、2、3、4、5......。


游戏行业新人的入门任务往往就是这些看起来微不足道,但是却非常细节的问题。正所谓不积跬步无以至千里,通过慢慢处理这些小小的bug和显示异常的问题,无疑是熟悉项目的最好方式。虽然这些都是比较基本的内容,修改bug、调整UI、修复报错。但是能够体现一个新人的基本功:阅读问题描述、理解问题描述、定位问题所在系统、定位系统所在代码、读懂代码原有逻辑、修改错误代码、验证问题是否修复、思考会不会对其他内容造成影响。


这对于管理者来说是非常合理的,但对于新人来说,未免太过于简单了。


支线任务


4.支线任务


游戏行业新人入门有可能并不能第一时间接触到游戏项目主分支的代码,往往是参与其他的一些分支版本,例如审核服(专门为了应对平台审核员的审核搭建的游戏服)、版署服(用于申请版号专门搭建的游戏服)、海外服(主要负责多语言版本的语言提取、翻译替换、本地化处理)等等。


安排新人去处理这些支线任务,为的就是让新人从另外一个相对安全的分支去熟悉游戏项目,避免因新人的处理不当造成线上版本出问题,从而造成公司的经济损失。支线任务通常就是枯燥单一的体力劳动,不需要过多的技巧,只需要耐得住寂寞的心。


图片源于网络


5.几点下班


一位有着远大抱负的新人,往往在刚进入公司的日子里,不知道几点下班。领导分配给我的任务,实在太简单了,三两下就完成了,还不到规定的时间。为了能够更加快速地熟悉项目,参与游戏功能的开发,继续研究代码。


HR说19点下班,但是18点的时候大家都跑去吃饭,不解,跟着。等到19点的时候,果然没有人下班。继续奋笔疾书。20点的时候终于有人下班了,可是领导还是没动静,算了,再看看代码吧。21点,领导好像发现了这个新人,让他早点回去休息。(没有人告诉他,这将是常态。) "没事,我再看会代码,马上就回去了。"


手机先吃


6.福利


同事: “公司发月饼了,你没去领吗?”,“不知道啊,没人通知。我刚来几天。”


同事: ”我看大家都去领了,现在。“,”我不知道自己是否算正式员工“


同事: ”你先去看看吧,反正大家都在领。“,兴致冲冲地跑到发月饼的地方。


发月饼的: “叫什么名字?”,”XXX“


发月饼的: ”名单上没这个人,不能领!“


刚加入公司的时候,可能由于没转正或者名字还没有进入公司的名册,往往会导致有些福利不能享受。例如公司发月饼的时候,人人有份,唯独你。又或者公司发奖金,你拿200慰问金。公司发年终奖,你还是拿慰问金。 但是如果你想请假,领导秒批。甚至说你想离职,领导也是轻描淡写,“好的”。没有丝毫的牵挂留恋。这是前所未有的福利。


结语


不管怎样,虽然你是公司的最底层,但你是公司中最坚实的基石,因为你在每一颗砖石上都留下了你的汗水和努力,为了让整座大厦能够稳固地矗立在成功的巅峰。加油,请认真工作,积极向上。


AD:笔者已经上线的小游戏《贪吃蛇掌机经典》《填色之旅》《重力迷宫球》大家可以自行点击搜索体验。


感兴趣的小伙伴记得关注微信公众号"亿元程序员"哦,一位有着8年游戏行业经验的主程。学习游戏开发不迷路。感谢您的关注,希望能给到您帮助, 也希望通过您能帮助到大家。


喜欢的可以点个、点个在看哦!请把该文章分享给你觉得有需要的其他小伙伴。谢谢。


作者:亿元程序员
来源:juejin.cn/post/7281589318329925689
收起阅读 »

uCharts 小程序地图下钻功能

web
uCharts 小程序地图下钻功能 最近在研究小程序图表,其中提到了一个地图下钻的功能,感觉挺有意思的,分享一下共同学习! 项目简介 这个Uni-App项目旨在提供一个可交互的地图,允许用户在中国地图的不同层级之间自由切换。用户可以从国家地图开始,然后深入到各...
继续阅读 »

uCharts 小程序地图下钻功能


最近在研究小程序图表,其中提到了一个地图下钻的功能,感觉挺有意思的,分享一下共同学习!


项目简介


这个Uni-App项目旨在提供一个可交互的地图,允许用户在中国地图的不同层级之间自由切换。用户可以从国家地图开始,然后深入到各省份地图,最终进入城市地图,点击不同区域/县级市查看详细信息。


下面是最终效果图👇👇


1695175245503.png


文档地址



准备工作


在开始之前,请确保你已经安装了Vue.js和Uni-App


并且准备好了模拟地图数据。这些数据将用于绘制地图。


地图数据遵循geoJson地图数据交换格式。如果你不熟悉geoJson,可以参考这里


绘制中国地图


// 首先引入我们的mock数据
import mockData from '../../mock/index'

// onLoad中调用 drawChina 方法来绘制中国地图
drawChina() {
uni.setNavigationBarTitle({
title: '中国地图'
});
setTimeout(() => {
let series = mockData.china.features;
// 这里循环一下series,把需要的数据增加到serie的属性中,fillOpacity是根据数据来显示的颜色层级透明度
for (var i = 0; i < series.length; i++) {
// 这里模拟了随机数据,实际开发中请根据实际情况修改
series[i].value = Math.floor(Math.random() * 1000)
series[i].fillOpacity = series[i].value / 1000
series[i].color = "#0D9FD8"
}
// 这里把series赋值给chartData,这样就可以在页面中渲染出来了
this.chartData = {
series: series
};
}, 100);
}

uCharts组件使用


插件导入后在uni_modules中,命名规则符合easyCom,可以直接在页面中使用


<qiun-data-charts
type="map"
canvas2d=""
:chartData="chartData"
:opts="opts"
:inScrollView="true"
:pageScrollTop="pageScrollTop"
tooltipFormat="mapFormat"
@getIndex="getIndex"
@complete="complete"
/>

注释说明:



  • chartData 包含地图数据

  • opts 是我们在 data 中定义的配置项

  • tooltipFormat 类型为字符串,需要指定为 config-ucharts.jsconfig-echarts.js 中 formatter 下的属性值,
    这里我们使用了 mapFormat,可以在 config-ucharts.js 中查看

  • 在页面中必须传入 pageScrollTop,并将 inScrollView 设置为 true,否则可能导致某些地图事件无法触发


事件说明:



  • @complete 事件是地图绘制完成后触发的事件,我们可以在这个事件中获取地图的实例,
    然后可以调用地图的方法进行进一步操作。

  • @getIndex 事件是地图点击事件,我们可以获取到点击的地图信息,
    根据这个信息来判断是否需要进行下钻操作,如果需要下钻,可以替换 chartData 并重新绘制地图。


下钻操作


  // 点击地图获取点击的索引
getIndex(e) {
console.log('点击地图', e);
if (e.currentIndex > -1) {
switch (this.layout) {
case 'china':
this.layout = 'province';
break;
case 'province':
this.layout = 'city';
break;
case 'city':
this.layout = 'area';
break;
default:
uni.showModal({
title: '提示',
content: '当前已经是最后一级地图,点击空白回到中国地图',
success: () => {

}
});
break;
}

this.drawNext(e.currentIndex);
} else {
this.layout = 'china';
this.drawChina();
}
}

以上代码中,我们通过 currentIndex 来判断当前点击的是哪个地图,然后根据 layout 的值来判断是否需要进行下钻操作。
如果需要下钻,我们就调用 drawNext 方法来绘制下一级地图。


这个demo中,我们只模拟了中国地图、省级地图、市级地图和区县级地图,如果在开发中我们需要根据adcode请求后端接口来获取地图数据


具体代码:git仓库地址


作者:养乐多多多
来源:juejin.cn/post/7278945628905226275
收起阅读 »

因为你没有正确面对问题,所以遍体鳞伤在所难免!

昨天在开发过程中遇到一个问题,排查到晚上12点,今天又排查了一天,到了晚上九点的时候,经过与同事的不断讨论,验证,终于发现了问题! 遇到问题是常态,在每个阶段对问题的思考方式也不一样,面对问题有两种选择,要么撒手不干,要么就硬着头皮去解决,当解决了一个又一个问...
继续阅读 »

昨天在开发过程中遇到一个问题,排查到晚上12点,今天又排查了一天,到了晚上九点的时候,经过与同事的不断讨论,验证,终于发现了问题!


遇到问题是常态,在每个阶段对问题的思考方式也不一样,面对问题有两种选择,要么撒手不干,要么就硬着头皮去解决,当解决了一个又一个问题的时候,你会发现自己成长了好多,技术只是一部分,我觉得只能占20%,而你的思考能力,你的认知,你的心态将会得到很大的提升,这占80%,这个世界上还有比进步更美好的事情吗?


但是解决问题是要有策略的,如果想单靠自己的能力去解决问题,我觉得不大可能,人在不同阶段,不同环境是十分受限的,因此除了打铁还得本身硬之外,我觉得更重要的是要学会“借力”,下面从自己的经历中总结几点!


1.学会不要脸


我觉得问题部分简单与否,傻逼与否,只要你肯问,基本上没人会拒绝回答你,不过得有一个前提,就是自己的确深度去思考了,闷只能让自己陷入深渊!


记得刚毕业进入公司的时候,分配了一个业务很复杂的问题,加上祖传代码,令人痛不欲生,虽然理清了代码,但是业务不太懂,问了同事几次,还是不太清楚,后面脆弱的自尊心作怪,不好意思继续问了,总怕别人觉得自己傻逼,于是就埋头苦干,我还记得下班后回到出租屋,深夜都在研究,但是问题越来越多,后面厚着脸皮去问,才把很多问题解决!


所以其实很多时候问题并不是很难,只要去和别人多沟通,沟通能解决百分之七八十的问题!


2.众里寻它千百度,问题出在自己处


问题有时候就出在眼前,只是自己没去去仔细观察,千里之行始于足下,有时候出现问题,我们总是会说这可能是别的地方有问题从而影响自己这边,但是却很少先从自己下手,一个很简单的逻辑,当自己加入后,出现问题了,那么很大程度是从自己这里开始的!


所以在觉得其他地方不对的时候得先审视自己,将自己的东西详细考虑后,确定没问题后再去怀疑其他地方,大多时候肯定是自己这里出问题,可能是粗心,也可能是基础不扎实,逻辑混乱等!


我们可以相信自己,但是也要学会怀疑自己,只有持怀疑的态度才能进步,如果有人给你说,“你完全不用怀疑我”,那么这人大概率也不怎么样!


3.你今天觉得是问题,过段时间就不是啥问题


我觉得心态是最重要的,很多人需要问题解决不了,就会陷入焦虑,害怕工作受影响,害怕学业受影响,从而睡不好觉!


很多同学找我毕业设计辅导的时候说自己焦虑得睡不着觉,害怕毕不了业,其实大可不必,因为焦虑是一个死循环,会对你的身心产生很大的影响,从而工作和学习的效率就会变得很低,有的时候无所谓一点比较好!


当实在做不了啦,那就别去做了,出去狂欢一下,把烦恼释放出去,心态整理好,然后再回来继续做,有时候我自己在做一些东西的时候,十分痛苦,也会很焦虑,但是当我跑出去看看山水,跑跑步,让自己的大脑和身体放松下来后,我再去解决这个问题,很快就能解决了!


每当遇到问题,我总会给自己说,现在这个问题你觉得是个问题,但是一段时间过后,你会发现这啥也不是,你会觉得为啥自己那时候会那么焦虑呢!


所以天塌下来了,该吃饭就吃饭,该睡觉就睡觉,别让工作和学业绑架自己,因为生活才是最重要的,如果真的实在干不了,觉得自己扛不住,身心受到很大影响,那就暂时撂挑子不干了!


你觉得呢!


今天的分享就到这里,感谢你的观看,下期见!


作者:刘牌
来源:juejin.cn/post/7281113939124453376
收起阅读 »

谈谈我家的奇葩买房经历

我是 2017 年毕业的,18 年买的房。 当时 IT 行业还是如日中天,薪资确实很高,我刚毕业就有接近 40 万。 当时的房价也是一路飙升,一周一个价那种。 我有个同事那年在北京买了房,犹豫了一周,涨了 20 多万。 那年过年回家的时候,我爸问我存了多少钱,...
继续阅读 »

我是 2017 年毕业的,18 年买的房。


当时 IT 行业还是如日中天,薪资确实很高,我刚毕业就有接近 40 万。


当时的房价也是一路飙升,一周一个价那种。


我有个同事那年在北京买了房,犹豫了一周,涨了 20 多万。


那年过年回家的时候,我爸问我存了多少钱,我说没有存多少,不知道钱花在哪里了。


我爸嫌我花的太多了,说要不买个房吧,这样每月还房贷还能存下点。


我说北京的房子需要交 5 年公积金才能买,而且首付二百多万呢,还没那么多钱。况且以后我也不一定留在北京,可能回青岛干。


年后我就回京继续上班了。


我爸在家开了一个门店,每天坐在门口和邻居聊天。


邻居聊起他儿子读完博士在青岛工作了,在黄岛区买了个房子,两周涨了十多万呢。


然后我爸就急了,非让我妈也去买一个,说是现在买还便宜点,就算我以后不回青岛,也可以卖了去北京再买。


我爸和我妈其实关系并不好,几乎是连吵带骂的逼着我妈去买。


为什么他不自己去呢?


因为我爸有严重的晕车,一坐车就吐。


我妈其实也没出过远门,自己一个人坐车从潍坊去青岛买房确实难为她了。


我妈还有点迷信,临走之前找算卦的占了一卦,算出一个方位,说是去青岛的城阳区买。


然后我妈就去了。


我妈啥也不懂,就在一个小区门口转悠。


然后保安过来问她干啥的。


她说想来买房,但是不知道去哪里买。


保安说我给你介绍一个人,可以找他买。


然后就给我妈介绍了一个中介。


那个中介说现在青岛都限购,需要交 2 年社保,只有即墨不限购,因为它刚撤市划区,划入青岛。


然后我妈找了个出租,并且给了出租的 200 块钱,让他一起去。


之后就到了即墨观澜国际的售楼处,人家介绍说这个房子是楼王,也就是最中间的那栋楼,是最好的,而且只有几套了。


我妈还在纠结,但是那个出租不耐烦了,要走。


然后我妈就定下来了,交了 70 万首付。


之后要办理手续,我从北京回家了一趟,和我爸我妈一起打车去了青岛。


我爸一路吐了有几十次,他说把胆汁都吐出来了。


就这样,我们就在青岛买下了这套房子。


13380 一平,首付 70 万,贷款 100 万,还 15 年,总共还 150 万。


然后我又回北京上班去了,只不过开始了还房贷的日子,一个月 1 万。


之后我爸又给了我 30 万,加上我自己还的,差不多在 2021 年就把 100 多万贷款还完了。因为提前还还的少。


差不多我爸 100 万,我拿了 100 万。


其实我还挺震惊的,我爸这样一个吃喝都那么节俭的人,竟然能拿出 100 万现金来。


后来在 2022 年年中的时候,我爸浑身疼的厉害,在地上打滚那种疼,去医院查出来是淋巴癌晚期。


然后 2023 年也就是今年年初的时候,我爸去世了。


去世前交代了一些事情,这套房子给我的 100 万就是他一辈子的积蓄了。


二手房要满 5 年才能卖,正好今年可以卖了。


但是问了下房价,现在观澜国际的均价是 7000 多,我们 2018 年买的时候是 13380 呢。而且 200 万的房子现在 90 万都不一定卖出去。


那我能咋办?


怪我爸?但我爸已经没了。


怪我妈?我妈也经常犯愁,而且当年是我爸逼她去的。


而且当年那种情况,我爸做的决定并没有错,当年大多数人都会认为房价会一直涨,早上车省很多钱。


我身边有一些朋友也是为了这个刚毕业不久就买房了。


其实住的话倒也没啥问题,关键是我并不去青岛工作,而且即墨那边也找不到前端的工作,互联网公司就集中在那几个城市。


租的话,一年才 1 万 5,而且装修还要投入好几万。


所以只能卖了。


本来是我们打算 5 年后卖了,正好在北京交满了 5 年公积金,然后再去北京买。


现在这情况,估计 200 万可能一分也收不回来。


遇到这事,正常人都会难受吧,我也一样。


那天我公众号发了条卖房消息:



真的是为了卖房么?


肯定不是啊,这样能把房子卖出去就怪了。


我只是想把它讲出来,仅此而已。讲出来之后确实好多了。


这几年我这种情况的全国也有不少:



并不是为了炒房,但确实因为各种原因不去住了。结果再卖的时候腰斩都卖不出去。


后来我也释然了,我本身物欲就很低,一辈子也用不了多少钱。


而且我还年轻,赚的也不少,可以再攒。


更重要的是,我一直觉得人这一生不能只是为了赚钱,要找到自己热爱的事业,在这个方向上持续开拓,创造自己的价值。


所幸我找到了。它才是支撑起我后半生的骨架。


最后,这段经历也不是完全没价值,至少我可以把它写下来,当做故事讲给你们听。


作者:zxg_神说要有光
来源:juejin.cn/post/7281833142104948776
收起阅读 »

因为你没有正确面对问题,所以遍体鳞伤在所难免!

昨天在开发过程中遇到一个问题,排查到晚上12点,今天又排查了一天,到了晚上九点的时候,经过与同事的不断讨论,验证,终于发现了问题! 遇到问题是常态,在每个阶段对问题的思考方式也不一样,面对问题有两种选择,要么撒手不干,要么就硬着头皮去解决,当解决了一个又一个问...
继续阅读 »

昨天在开发过程中遇到一个问题,排查到晚上12点,今天又排查了一天,到了晚上九点的时候,经过与同事的不断讨论,验证,终于发现了问题!


遇到问题是常态,在每个阶段对问题的思考方式也不一样,面对问题有两种选择,要么撒手不干,要么就硬着头皮去解决,当解决了一个又一个问题的时候,你会发现自己成长了好多,技术只是一部分,我觉得只能占20%,而你的思考能力,你的认知,你的心态将会得到很大的提升,这占80%,这个世界上还有比进步更美好的事情吗?


但是解决问题是要有策略的,如果想单靠自己的能力去解决问题,我觉得不大可能,人在不同阶段,不同环境是十分受限的,因此除了打铁还得本身硬之外,我觉得更重要的是要学会“借力”,下面从自己的经历中总结几点!


1.学会不要脸


我觉得问题部分简单与否,傻逼与否,只要你肯问,基本上没人会拒绝回答你,不过得有一个前提,就是自己的确深度去思考了,闷只能让自己陷入深渊!


记得刚毕业进入公司的时候,分配了一个业务很复杂的问题,加上祖传代码,令人痛不欲生,虽然理清了代码,但是业务不太懂,问了同事几次,还是不太清楚,后面脆弱的自尊心作怪,不好意思继续问了,总怕别人觉得自己傻逼,于是就埋头苦干,我还记得下班后回到出租屋,深夜都在研究,但是问题越来越多,后面厚着脸皮去问,才把很多问题解决!


所以其实很多时候问题并不是很难,只要去和别人多沟通,沟通能解决百分之七八十的问题!


2.众里寻它千百度,问题出在自己处


问题有时候就出在眼前,只是自己没去去仔细观察,千里之行始于足下,有时候出现问题,我们总是会说这可能是别的地方有问题从而影响自己这边,但是却很少先从自己下手,一个很简单的逻辑,当自己加入后,出现问题了,那么很大程度是从自己这里开始的!


所以在觉得其他地方不对的时候得先审视自己,将自己的东西详细考虑后,确定没问题后再去怀疑其他地方,大多时候肯定是自己这里出问题,可能是粗心,也可能是基础不扎实,逻辑混乱等!


我们可以相信自己,但是也要学会怀疑自己,只有持怀疑的态度才能进步,如果有人给你说,“你完全不用怀疑我”,那么这人大概率也不怎么样!


3.你今天觉得是问题,过段时间就不是啥问题


我觉得心态是最重要的,很多人需要问题解决不了,就会陷入焦虑,害怕工作受影响,害怕学业受影响,从而睡不好觉!


很多同学找我毕业设计辅导的时候说自己焦虑得睡不着觉,害怕毕不了业,其实大可不必,因为焦虑是一个死循环,会对你的身心产生很大的影响,从而工作和学习的效率就会变得很低,有的时候无所谓一点比较好!


当实在做不了啦,那就别去做了,出去狂欢一下,把烦恼释放出去,心态整理好,然后再回来继续做,有时候我自己在做一些东西的时候,十分痛苦,也会很焦虑,但是当我跑出去看看山水,跑跑步,让自己的大脑和身体放松下来后,我再去解决这个问题,很快就能解决了!


每当遇到问题,我总会给自己说,现在这个问题你觉得是个问题,但是一段时间过后,你会发现这啥也不是,你会觉得为啥自己那时候会那么焦虑呢!


所以天塌下来了,该吃饭就吃饭,该睡觉就睡觉,别让工作和学业绑架自己,因为生活才是最重要的,如果真的实在干不了,觉得自己扛不住,身心受到很大影响,那就暂时撂挑子不干了!


你觉得呢!


今天的分享就到这里,感谢你的观看,下期见!


作者:刘牌
链接:https://juejin.cn/post/7281113939124453376
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter 实现登录 UI

本文,我将解析怎么前构建一个用户交互的登录页面。这里,我使用 TextField 挂件,这方便用户输入用户名和密码。还使用 FlatButton 挂件,来处理一些动作。当然,我还使用了 Image 挂件来设定登录页面的 logo。 效果图如下: 第一步: m...
继续阅读 »

本文,我将解析怎么前构建一个用户交互的登录页面。这里,我使用 TextField 挂件,这方便用户输入用户名和密码。还使用 FlatButton 挂件,来处理一些动作。当然,我还使用了 Image 挂件来设定登录页面的 logo


效果图如下:




第一步: main() 函数

import 'package:flutter/material.dart';void main() {
runApp(MyApp());
}class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: LoginDemo(),
);
}
}

这个 main() 函数也就是应用的入口。MyApp 类中添加了一个 LoginDemo 类作为 home 属性的参数。


第二步:class LoginDemo


  • 设定脚手架的 appBar 属性来作为应用的标题,如下:
appBar: AppBar(
title: Text('Login Page'),
),

  • 在本次的 UI 布局中,所有的挂件都会放在 Column 挂件中,然后存放在脚手架的 body 中。Column 中的第一个是存放 Container 挂件,用来处理 Image 挂件。
Container(
height: 150.0,
width: 190.0,
padding: EdgeInsets.only(top: 40),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(200),
),
child: Center(
child: Image.asset('asset/images/flutter-logo.png'),
),
),

flutter-logo.png 文件存放在 asset/images 文件夹中。我们需要在 pubspec.yaml 文件中配置路径。

# To add assets to your application, add an assets section, like this:
assets:
- asset/images/



添加完资源之后,我们可以运行应用了。


  • 然后,使用 TextField 挂件处理用户名和密码。 TextField 挂件是一个输入挂件,帮助我们处理用户的输入信息。
Padding(
padding: EdgeInsets.all(10),
child: TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'User Name',
hintText: 'Enter valid mail id as abc@gmail.com'
),
),
),
Padding(
padding: EdgeInsets.all(10),
child: TextField(
obscureText: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
hintText: 'Enter your secure password'
),
),
),

这里的 Padding 挂件能够帮助你设定 TextField 挂件的内边距。



obscureText 属性值为 true 的时候,帮助我们对 TextField 展示特殊的字符,而不是真正的文本。



  • 我们使用 FlatButton 挂件来处理忘记密码
FlatButton(
onPressed: (){
//TODO FORGOT PASSWORD SCREEN GOES HERE
},
child: Text(
'Forgot Password',
style: TextStyle(color: Colors.blue, fontSize: 15),
),
),

onPressed() 这个函数中,我们可以处理页面跳转或者其他的点击逻辑。


  • 对于登录按钮,我们使用 FlatButton 挂件,但是我们得装饰一下,这里我们使用 Container 进行包裹。
Container(
height: 50,
width: 250,
decoration: BoxDecoration(
color: Colors.*blue*, borderRadius: BorderRadius.circular(20),
),
child: FlatButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => HomePage()),
);
},
child: Text(
'Login',
style: TextStyle(color: Colors.*white*, fontSize: 25),
),
),
),

上面我们设定了 Container 挂件的 heightwidth 属性,所以 flatbutton 也会获取到相同的高度和宽度。


decoration 属性允许我们设计按钮,比如颜色 colorColors.blueborderRadiusBorderRadius.circular(20) 属性。


  • 最后指定 Text 挂件以为新用户创建账号

这里我们可以通过 GestureDetector 挂件的 onTap() 功能进行导航操作。或者创建类似忘记密码按钮的 onPressed() 事件。


这里是整个项目的完整代码:

// lib/HomePage.dart

import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Page'),
),
body: Center(
child: Container(
height: 80,
width: 150,
decoration: BoxDecoration(
color: Colors.blue, borderRadius: BorderRadius.circular(10)),
child: FlatButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(
'Welcome',
style: TextStyle(color: Colors.white, fontSize: 25),
),
),
),
),
);
}
}
// lib/main.dart
import 'package:flutter/material.dart';

import 'HomePage.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: LoginDemo(),
);
}
}

class LoginDemo extends StatefulWidget {
@override
_LoginDemoState createState() => _LoginDemoState();
}

class _LoginDemoState extends State<LoginDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("Login Page"),
),
body: SingleChildScrollView(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 60.0),
child: Center(
child: Container(
width: 200,
height: 150,
/*decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(50.0)),*/
child: Image.asset('asset/images/flutter-logo.png')),
),
),
Padding(
//padding: const EdgeInsets.only(left:15.0,right: 15.0,top:0,bottom: 0),
padding: EdgeInsets.symmetric(horizontal: 15),
child: TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email',
hintText: 'Enter valid email id as abc@gmail.com'),
),
),
Padding(
padding: const EdgeInsets.only(
left: 15.0, right: 15.0, top: 15, bottom: 0),
//padding: EdgeInsets.symmetric(horizontal: 15),
child: TextField(

obscureText: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
hintText: 'Enter secure password'),
),
),
FlatButton(
onPressed: (){
//TODO FORGOT PASSWORD SCREEN GOES HERE
},
child: Text(
'Forgot Password',
style: TextStyle(color: Colors.blue, fontSize: 15),
),
),
Container(
height: 50,
width: 250,
decoration: BoxDecoration(
color: Colors.blue, borderRadius: BorderRadius.circular(20)),
child: FlatButton(
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (_) => HomePage()));
},
child: Text(
'Login',
style: TextStyle(color: Colors.white, fontSize: 25),
),
),
),
SizedBox(
height: 130,
),
Text('New User? Create Account')
],
),
),
);
}
}


本文采用意译的方式翻译。原文 levelup.gitconnected.com/login-page-…



推荐阅读

作者:Jimmy
链接:https://juejin.cn/post/7147865530869907487
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

在这个大环境下我是如何找工作的

蛮久没更新了,本次我想聊聊找工作的事情,相信大家都能感受到从去年开始到现在市场是一天比一天差,特别是在我们互联网 IT 行业。 已经过了 18 年之前的高速发展的红利期,能做的互联网应用几乎已经被各大公司做了个遍,现在已经进入稳定的存量市场,所以在这样的大背景...
继续阅读 »

蛮久没更新了,本次我想聊聊找工作的事情,相信大家都能感受到从去年开始到现在市场是一天比一天差,特别是在我们互联网 IT 行业。
已经过了 18 年之前的高速发展的红利期,能做的互联网应用几乎已经被各大公司做了个遍,现在已经进入稳定的存量市场,所以在这样的大背景下再加上全世界范围内的经济不景气我想每个人都能感受到寒意。


我还记得大约在 20 年的时候看到网上经常说的一句话:今年将是未来十年最好的一年。


由于当时我所在的公司业务发展还比较顺利,丝毫没有危机意识,对这种言论总是嗤之以鼻,直到去年国庆节附近。


虽然我们做的是海外业务,但是当时受到各方面的原因公司的业务也极速收缩(被收购,资本不看好),所以公司不得不进行裁员;
其实到我这里的时候前面已经大概有 2~3 波的优化,我们是最后一波,几乎等于是全军覆没,只留下少数的人维护现有系统。


这家公司也是我工作这么多年来少数能感受到人情味的公司,虽有不舍,但现实的残酷并不是由我们个人所决定的。


之后便开始漫长的找工作之旅,到现在也已经入职半年多了;最近看到身边朋友以及网上的一些信息,往往是坏消息多于好消息。


市场经历半年多的时间,裁员的公司反而增多,岗位也越来越少,所以到现在不管是在职还是离职的朋友或多或少都有所焦虑,我也觉得有必要分享一下我的经历。


我的预期目标


下面重点聊聊找工作的事情;其实刚开始得知要找工作的时候我并不是特别慌,因为当时手上有部分积蓄加上公司有 N+1 的赔偿,同时去年 10 月份的时候岗位相对于现在还是要多一些。


所以我当时的目标是花一个月的时间找一个我觉得靠谱的工作,至少能长期稳定的工作 3 年以上。


工作性质可以是纯研发或者是偏管理岗都可以,结合我个人的兴趣纯研发岗的话我希望是可以做纯技术性质的工作,相信大部分做业务研发的朋友都希望能做一些看似“高大上”的内容。
这一点我也不例外,所以中间件就和云相关的内容就是我的目标。


不过这点在重庆这个大洼地中很难找到对口工作,所以我的第二目标是技术 leader,或者说是核心主程之类的,毕竟考虑到 3 年后我也 30+ 了,如果能再积累几年的管理经验后续的路会更好走一些。


当然还有第三个选项就是远程,不过远程的岗位更少,大部分都是和 web3,区块链相关的工作;我对这块一直比较谨慎所以也没深入了解。


找工作流水账


因为我从入职这家公司到现在其实还没出来面试过,也不太知道市场行情,所以我的想法是先找几家自己不是非去不可的公司练练手。



有一个我个人的偏好忘记讲到,因为最近的一段时间写 Go 会多一些,所以我优先看的是 Go 相关的岗位。



第一家


首先第一家是一个 ToB 教育行业的公司,大概的背景是在重庆新成立的研发中心,技术栈也是 Go;


我现在还记得最后一轮我问研发负责人当初为啥选 Go,他的回答是:



Java 那种臃肿的语言我们首先就不考虑,PHP 也日落西山,未来一定会是 Go 的天下。



由于是新成立的团队,对方发现我之前有管理相关的经验,加上面试印象,所以是期望我过去能做重庆研发 Leader。


为此还特地帮我申请了薪资调整,因为我之前干过 ToB 业务,所以我大概清楚其中的流程,这种确实得领导特批,所以最后虽然没成但依然很感谢当时的 HR 帮我去沟通。


第二家


第二家主要是偏年轻人的 C 端产品,技术栈也是 Go;给我印象比较深的是,去到公司怎么按电梯都不知道🤣



他们办公室在我们这里的 CBD,我长期在政府赞助的产业园里工作确实受到了小小的震撼,办公环境比较好。



当然面试过程给我留下的印象依然非常深刻,我现在依然记得我坐下后面试官也就是 CTO 给我说的第一句话:



我看过你的简历后就决定今天咱们不聊技术话题了,直接聊聊公司层面和业务上是否感兴趣,以及解答我的疑虑,因为我已经看过你写的很多博客和 GitHub,技术能力方面比较放心。



之后就是常规流程,聊聊公司情况个人意愿等。


最后我也问了为什么选 Go,这位 CTO 给我的回答和上一家差不多😂


虽然最终也没能去成,但也非常感谢这位 CTO,他是我碰到为数不多会在面试前认真看你的简历,博客和 GitHub 都会真的点进去仔细阅读👍🏼。



其实这两家我都没怎么讲技术细节,因为确实没怎么聊这部分内容;这时就突出维护自己的技术博客和 GitHub 的优势了,技术博客我从 16 年到现在写了大约 170 篇,GitHub 上开源过一些高 star 项目,也参与过一些开源项目,这些都是没有大厂经历的背书,对招聘者来说也是节约他的时间。



 


当然有好处自然也有“坏处”,这个后续会讲到。


第三家


第三家是找朋友推荐的,在业界算是知名的云原生服务提供商,主要做 ToB 业务;因为主要是围绕着 k8s 社区生态做研发,所以就是纯技术的工作,面试的时候也会问一些技术细节。



我还记得有一轮 leader 面,他说你入职后工作内容和之前完全不同,甚至数据库都不需要安装了。



整体大概 5、6 轮,后面两轮都是 BOSS 面,几乎没有问技术问题,主要是聊聊我的个人项目。


我大概记得一些技术问题:


  • k8s 相关的一些组件、Operator
  • Go 相关的放射、接口、如何动态修改类实现等等。
  • Java 相关就是一些常规的,主要是一些常用特性和 Go 做比较,看看对这两门语言的理解。

其实这家公司是比较吸引我的,几乎就是围绕着开源社区做研发,工作中大部分时间也是在做开源项目,所以可以说是把我之前的业余爱好和工作结合起来了。


在贡献开源社区的同时还能收到公司的现金奖励,不可谓是双赢。


对我不太友好的是工作地在成都,入职后得成渝两地跑;而且在最终发 offer 的前两小时,公司突然停止 HC 了,这点确实没想到,所以阴差阳错的我也没有去成。


第四家


第四家也就是我现在入职的公司,当时是我在招聘网站上看到的唯一一家做中间件的岗位,抱着试一试的态度我就投了。
面试过程也比较顺利,一轮同事面,一轮 Leader 面。


技术上也没有聊太多,后来我自己猜测大概率也和我的博客和 Github 有关。




当然整个过程也有不太友好的经历,比如有一家成都的“知名”旅游公司;面试的时候那个面试官给我的感觉是压根没有看我的简历,所有的问题都是在读他的稿子,根本没有上下文联系。


还有一家更离谱,直接在招聘软件上发了一个加密相关的算法,让我解释下;因为当时我在外边逛街,所以没有注意到消息;后来加上微信后说我为什么没有回复,然后整个面试就在微信上打字进行。


其中问了一个很具体的问题,我记得好像是 MD5 的具体实现,说实话我不知道,从字里行间我感觉对方的态度并不友好,也就没有必要再聊下去;最后给我说之所以问这些,是因为看了我的博客后觉得我技术实力不错,所以对我期待较高;我只能是地铁老人看手机。


最终看来八股文确实是绕不开的,我也花了几天时间整理了 Java 和 Go 的相关资料;不过我觉得也有应对的方法。


首先得看你面试的岗位,如果是常见的业务研发,从招聘的 JD 描述其实是可以看出来的,比如有提到什么 Java 并发、锁、Spring等等,大概率是要问八股的;这个没办法,别人都在背你不背就落后一截了。


之后我建议自己平时在博客里多记录八股相关的内容,并且在简历上着重标明博客的地址,尽量让面试官先看到;这样先发制人,你想问的我已经总结好了😂。


但这个的前提是要自己长期记录,不能等到面试的时候才想起去更新,长期维护也能加深自己的印象,按照 “艾宾浩斯遗忘曲线” 进行复习。


选择



这是我当时记录的面试情况,最终根据喜好程度选择了现在这家公司。


不过也有一点我现在觉得但是考虑漏了,那就是行业前景。


现在的 C 端业务真的不好做,相对好做的是一些 B 端,回款周期长,同时不太吃现金流;这样的业务相对来说活的会久一些,我现在所在的公司就是纯做 C 端,在我看来也没有形成自己的护城河,只要有人愿意砸钱随时可以把你干下去。


加上现在的资本也不敢随意投钱,公司哪天不挣钱的话首先就是考虑缩减产研的成本,所以裁员指不定就会在哪一天到来。


现在庆幸的是入职现在这家公司也没有选错,至少短期内看来不会再裁员,同时我做的事情也是比较感兴趣的;和第三家有些许类似,只是做得是内部的基础架构,也需要经常和开源社区交流。


面对裁员能做的事情


说到裁员,这也是我第一次碰上,只能分享为数不多的经验。


避免裁员


当然第一条是尽量避免进入裁员名单,这个我最近在播客 作为曾经的老板,我们眼中的裁员和那些建议 讲到在当下的市场情况下哪些人更容易进入裁员名单:


  • 年纪大的,这类收入不低,同时收益也没年轻人高,确实更容易进入名单。
  • 未婚女性,这点确实有点政治不正确,但确实就是现在的事实,这个需要整个社会,政府来一起解决。
  • 做事本本分分,没有贡献也没出啥事故。
  • 边缘业务,也容易被优化缩减成本。

那如何避免裁员呢,当然首先尽量别和以上特征重合,一些客观情况避免不了,但我们可以在第三点上主动“卷”一下,当然这个的前提是你还想在这家公司干。


还有一个方法是提前向公司告知降薪,这点可能很多人不理解,因为我们大部分人的收入都是随着跳槽越来越高的;但这些好处是否是受到前些年互联网过于热门的影响呢?


当然个人待遇是由市场决定的,现在互联网不可否认的降温了,如果你觉得各方面呆在这家公司都比出去再找一个更好,那这也不失为一个方法;除非你有信心能找到一个更好的,那就另说了。


未来计划


我觉得只要一家公司只要有裁员的风声传出来后,即便是没被裁,你也会处于焦虑之中;要想避免这种焦虑确实也很简单,只要有稳定的被动收入那就无所谓了。


这个确实也是说起来轻松做起来难,我最近也一直在思考能不能在工作之余做一些小的 side project,这话题就大了,只是我觉得我们程序员先天就有自己做一个产品的机会和能力,与其把生杀大权给别人,不如握在自己手里。


当然这里得提醒下,在国内的企业,大部分老板都认为签了合同你的 24 小时都是他的,所以这些业务项目最好是保持低调,同时不能影响到本职工作。


作者:crossoverJie
链接:https://juejin.cn/post/7246570594991718455
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

当史上最强AI,遇上中国2023高考作文

今天是2023年高考第一天,刚过中午,坐在办公室里就收到了关于今年高考作文题的新闻推送。 要知道,高考作文题,历来都是能引发全国人民热议的一个话题,毕竟是写东西嘛,虽然不是所有人都能写好文章,但是绝大部分人多多少少都能写两句,面对高考作文都能发表下自己的见解。...
继续阅读 »

今天是2023年高考第一天,刚过中午,坐在办公室里就收到了关于今年高考作文题的新闻推送。


要知道,高考作文题,历来都是能引发全国人民热议的一个话题,毕竟是写东西嘛,虽然不是所有人都能写好文章,但是绝大部分人多多少少都能写两句,面对高考作文都能发表下自己的见解。


那么,当ChatGPT遇到高考作文,会发生什么呢?它能写出什么样的水平呢?


带着这个疑问,我决定试试。


我选取的是今年上海的作文题(我比较感兴趣),题目如下:




面对这个题目,不知道各位有什么想法么?如果你去考试,你会怎么写?


来,我们看看AI是怎么写的。


以下是GPT-4的作文,大家品鉴一下:




我先说下我的整体感受,然后再具体分析。


我就一个感受:我维持4个月前对于AI能力的基本判断,即「长短板都格外的明显」。




先说优点吧,全文非常顺畅,不仅有条理有举证,更重要的是有层次 当我还在思考,除了好奇心之外,还能从哪些角度去谈这个问题的时候,GPT几秒钟就给了我答案:


「一个人乐意去探索陌生世界,并非仅仅是因为好奇心,还有求知欲实现自我价值,和社会责任感


醍醐灌顶啊朋友们!


不知道你们有没有想到这么多角度,反正我这么短时间是想不出来的,太牛了!


关键是,这4个因素,是层层递进的!


“好奇心”尚且处于动物性 ,处于「本我」阶段,“求知欲”就具备了人性,迈入了「自我」,“实现自我价值”则是更进一步的「自我」,而到了“社会责任感”,就直接达到了「超我」的境界。


厉害了啊!


这就是我说的,它不仅仅是写的有条理,还有层次递进,这个太夸张了,可以说轻松超越99%的普通人。


好啦,彩虹屁吹完了,那么我再来说下缺点或者说劣势在哪。


其实相关观点我在之前的文章里已经表达过一次了,大家可以点这个看下:《不会出错的ChatGPT毫无意义》


还是那句话,GPT是以成年人的数据训练出来的,而成年人是数据具备了极强的逻辑性,也正因此,GPT最擅长逻辑推演,表达也极具逻辑性(所以它能层层递进)。


但是吧,强逻辑的东西自然就缺乏了人文美,缺乏浪漫和非理性想象力


不认可?好,我来给你证明一下。


为了试探GPT是否具备更多可能性,把文章写的更文艺一些,于是我换了个问法:




哦吼,翻车。除了题目稍微变了一下,剩下的内容几乎完全没有变化。


嗯,你可能要说了,是你问的不对。好,我再换个问法:




依然不行,依然是那副强逻辑的修辞和表达,我很失望,它不懂我的文艺。


也许确实是我问法不对,但我仍然坚定的认为,AI在人性的光芒面前,依然只是一台冰冷的机器,至少目前仍然是。


好啦,本文只是一个浅尝辄止的探讨,更多对于AI与人性的探讨,可以读读我之前的文章:


《不会出错的ChatGPT毫无意义》


作者:沐洒
链接:https://juejin.cn/post/7241874482574180411
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

在 iOS 中使用 IdentifyLookup 进行短信过滤

iOS
垃圾短信是一个长期存在、令人困扰的问题。本文将介绍如何阻止这些短信、设备端的检测以及整合动态的服务器检测等。 Apple 在 WWDC 2017(iOS 11) 推出了 IdentityLookup 框架,让开发者可以参与到过滤短信的过程中。在 iOS 14,...
继续阅读 »

垃圾短信是一个长期存在、令人困扰的问题。本文将介绍如何阻止这些短信、设备端的检测以及整合动态的服务器检测等。


Apple 在 WWDC 2017(iOS 11) 推出了 IdentityLookup 框架,让开发者可以参与到过滤短信的过程中。在 iOS 14,Apple 新增了两种过滤类别:交易信息(Promotion)、推广信息(transaction)。在 WWDC 2022(iOS 16),针对这两种类别,Apple 新增了 12 种子类别,推广信息包括 9 种子类别:其他(Others)、财务(Finance)、订单(Orders)、提醒(Reminders)、健康(Health)、天气(Weather)、运营商(Carrier)、奖励(Rewards)、公共服务(PublicServices)。交易信息包括 3 种子类别:其他(Others)、优惠(Offers)、优惠券(Coupons)。




消息过滤流程


消息过滤通过应用程序扩展(App extension)来完成。当用户收到来自未知发件人的消息时,“消息” APP 通过询问 Message Filter Extension,来确定该消息的类别。Message Filter Extension 可以通过使用内置逻辑或推迟到关联服务器的分析来做出此决定。



IdentityLookup 仅适用于来自未知发件人的短信和彩信,它不适用于联系人列表中发件人的消息、不适用任何 iMessage 消息、不适用于回复发件人 3 次及以上的会话。





“消息” APP 使用一个 ILMessageFilterQueryRequest 对象将信息传递给 Message Filter Extension。Message Filter Extension 确定该消息的类别后,将 ILMessageFilterQueryResponse 对象返回给“消息” APP。


如果 App extension 无法自行做出决策,“消息” APP将会把有关信息发送到与 Message Filter Extension 关联的服务器,并将响应传递给 Message Filter Extension。Message Filter Extension 解析服务器的响应并返回最终的 ILMessageFilterQueryResponse 对象,如下图所示。




出于隐私原因,系统会处理与关联的服务器的所有通信;Message Filter Extension 无法直接访问网络,也无法将数据写入应用的共享容器中。


消息过滤实践


为 APP 新增 Message Filter Extension:











我们依次来看 MessageFilterExtension.swift 文件中的代码:

import IdentityLookup
final class MessageFilterExtension: ILMessageFilterExtension {}

ILMessageFilterExtension 是的主要类的抽象基类。在 Info.plist 中被设置 NSExtensionPrincipalClass,将在收到消息时被构造:




ILMessageFilterExtension 类无其他要求或限制:

open class ILMessageFilterExtension : NSObject {
}

MessageFilterExtension 实现了 ILMessageFilterQueryHandlingILMessageFilterCapabilitiesQueryHandling 协议:

extension MessageFilterExtension: ILMessageFilterQueryHandling, ILMessageFilterCapabilitiesQueryHandling {
// ...
}

ILMessageFilterQueryHandling


ILMessageFilterExtension 子类必须符合 ILMessageFilterQueryHandling协议,通过包含短信信息的 queryRequest 、提供请求关联网络服务器能力的 context,来进行短信类别的判断。最终返回提供包含类别信息的 response

@available(iOS 11.0, *)
public protocol ILMessageFilterQueryHandling : NSObjectProtocol {
   // 闭包
   func handle(_ queryRequest: ILMessageFilterQueryRequest,
               context: ILMessageFilterExtensionContext,
               completion: @escaping (ILMessageFilterQueryResponse) -> Void)
// 异步函数
   func handle(_ queryRequest: ILMessageFilterQueryRequest,
               context: ILMessageFilterExtensionContext
   ) async -> ILMessageFilterQueryResponse
}

queryRequest 的信息如下,包括发件人号码 sender、短信内容 messageBodyISO 国家代码 receiverISOCountryCode

@available(iOS 11.0, *)
open class ILMessageFilterQueryRequest : NSObject, NSSecureCoding {
   open var sender: String? { get }
   open var messageBody: String? { get }
   @available(iOS 16.0, *)
   open var receiverISOCountryCode: String? { get }
}

context 提供请求关联网络服务器能力,我们也只能使用该能力访问网络:

@available(iOS 11.0, *)
open class ILMessageFilterExtensionContext : NSExtensionContext {
// 闭包
   open func deferQueryRequestToNetwork(completion: @escaping (ILNetworkResponse?, Error?) -> Void)
// 异步函数
   open func deferQueryRequestToNetwork() async throws -> ILNetworkResponse
}


URL 记录在 Info.plistILMessageFilterExtensionNetworkURL 中,无法进行自定义。





response 定义如下,需要提供对应的类别和子类别:

@available(iOS 11.0, *)
open class ILMessageFilterQueryResponse : NSObject, NSSecureCoding {
   open var action: ILMessageFilterAction
   @available(iOS 16.0, *)
   open var subAction: ILMessageFilterSubAction
}

noneallowjunkpromotiontransaction 类别,noneallow 的行为相同:

@available(iOS 11.0, *)
public enum ILMessageFilterAction : Int, @unchecked Sendable {
   case none = 0
   case allow = 1
   case junk = 2
   @available(iOS 14.0, *)
   case promotion = 3
   @available(iOS 14.0, *)
   case transaction = 4
}

以及文章开头提到的 12 种子类别:

@available(iOS 16.0, *)
public enum ILMessageFilterSubAction : Int, @unchecked Sendable {
   case none = 0
   
   /// TRANSACTIONAL SUB-ACTIONS
   
   case transactionalOthers = 10000
   case transactionalFinance = 10001
   case transactionalOrders = 10002
   case transactionalReminders = 10003
   case transactionalHealth = 10004
   case transactionalWeather = 10005
   case transactionalCarrier = 10006
   case transactionalRewards = 10007
   case transactionalPublicServices = 10008
   
   /// PROMOTIONAL SUB-ACTIONS
   
   case promotionalOffers = 20001
   case promotionalCoupons = 20002
}

因此,整体的过滤代码框架如下,依次进行设备端的检测、服务器检测:

func handle(_ queryRequest: ILMessageFilterQueryRequest,
           context: ILMessageFilterExtensionContext,
           completion: @escaping (ILMessageFilterQueryResponse) -> Void
) {
   // 设备端的检测
   let (offlineAction, offlineSubAction) = self.offlineAction(for: queryRequest)
   switch offlineAction {
   case .allow, .junk, .promotion, .transaction:
       let response = ILMessageFilterQueryResponse()
       response.action = offlineAction
       response.subAction = offlineSubAction
       completion(response)
   case .none:
      // 服务器检测
       context.deferQueryRequestToNetwork() { (networkResponse, error) in
           let response = ILMessageFilterQueryResponse()
           if let networkResponse = networkResponse {
               (response.action, response.subAction) = self.networkAction(for: networkResponse)
           }
           completion(response)
       }
   @unknown default:
       break
   }
}

这里需要注意,Apple 定义了服务器检测网络请求的格式,开发者无法进行自定义:

POST /server-endpoint HTTP/1.1
Accept: */*
Content-Type: application/json; charset=utf-8
Content-Length: 148
{
  "_version": 1,
  "query": {
      "sender": "14085550001",
      "message": {
          "text": "This is a message"
      }
  },
  "app": {
      "version": "1.1"
  }
}

ILMessageFilterCapabilitiesQueryHandling


ILMessageFilterCapabilitiesQueryHandling 协议会更简单些:

@available(iOS 16.0, *)
public protocol ILMessageFilterCapabilitiesQueryHandling : NSObjectProtocol {
 // 闭包
   func handle(_ capabilitiesQueryRequest: ILMessageFilterCapabilitiesQueryRequest,
               context: ILMessageFilterExtensionContext,
               completion: @escaping (ILMessageFilterCapabilitiesQueryResponse
   ) -> Void)
// 异步函数
   func handle(_ capabilitiesQueryRequest: ILMessageFilterCapabilitiesQueryRequest,
               context: ILMessageFilterExtensionContext
   ) async -> ILMessageFilterCapabilitiesQueryResponse
}

其中,capabilitiesQueryRequest 无实际含义,context 同前文。需要提供的是 ILMessageFilterCapabilitiesQueryResponse:

@available(iOS 16.0, *)
open class ILMessageFilterCapabilitiesQueryResponse : NSObject, NSSecureCoding {
}

@available(iOS 16.0, *)
@available(macOS, unavailable)
extension ILMessageFilterCapabilitiesQueryResponse {

   @nonobjc final public var transactionalSubActions: [ILMessageFilterSubAction]

   final public var promotionalSubActions: [ILMessageFilterSubAction]
}

指定了 Message Filter Extension 可以显示的子类别。我们可以这样展示以显示子类别:

func handle(_ capabilitiesQueryRequest: ILMessageFilterCapabilitiesQueryRequest,
           context: ILMessageFilterExtensionContext,
           completion: @escaping (ILMessageFilterCapabilitiesQueryResponse) -> Void
) {
   let response = ILMessageFilterCapabilitiesQueryResponse()
   response.transactionalSubActions = [        .transactionalOthers,        .transactionalFinance,        .transactionalOrders,        .transactionalReminders,        .transactionalHealth,        .transactionalWeather,        .transactionalCarrier,        .transactionalRewards,        .transactionalPublicServices    ]
   response.promotionalSubActions = [                .promotionalOthers,        .promotionalOffers,        .promotionalCoupons,    ]
   completion(response)
}

在 iOS16 设备上,不同配置样式如下:


垃圾短信和垃圾电话上报



此外,我们可以一个 App Extension,让用户将不需要的短信和电话上报为垃圾内容。上报电话需要用户在最近列表中进行左滑后选择报告。对于在消息记录中的短信,用户可以按下报告垃圾信息按钮:












创建 Unwanted Communication Reporting Extension:











我们可以看到模版代码十分简单:

class UnwantedCommunicationReportingExtension: ILClassificationUIExtensionViewController {
   
   override func viewDidAppear(_ animated: Bool) {
       super.viewDidAppear(animated)
       // Notify the system when you have completed gathering information
       // from the user and you are ready with a classification response
       self.extensionContext.isReadyForClassificationResponse = true
   }
   
   // Customize UI based on the classification request before the view is loaded
   override func prepare(for classificationRequest: ILClassificationRequest) {
       // Configure your views for the classification request
   }
   
   // Provide a classification response for the classification request
   override func classificationResponse(for request:ILClassificationRequest) -> ILClassificationResponse {
       return ILClassificationResponse(action: .reportJunk)
   }
}

当用户上报时,系统会启动 App Extension。搜集用户外信息后,进行后续的上报或阻止,如下图所示。




具体来说,系统会依次:


  1. 实例化 App Extension 中的 ILClassificationUIExtensionViewController 子类。
  2. 调用实例的 prepare(for:)`方法并将控制器呈现给用户。
    1. 使用实例从用户那里收集数据,搜集完成 isReadyForClassificationResponse 设置为 true
    2. 如果用户按下取消按钮,系统将关闭 ILClassificationUIExtensionViewController 子类实例。

    1. 如果用户按下完成,系统将调用 classificationResponse(for:) 方法,传入一个 ILClassificationRequest 对象。

    1. 系统根据方法的 ILClassificationResponse 响应采取不同的操作。
@available(iOS 12.0, *)
open class ILClassificationResponse : NSObject, NSSecureCoding {
   open var action: ILClassificationAction { get }
   @available(iOS 12.1, *)
   open var userString: String?
   open var userInfo: [String : Any]?
   public init(action: ILClassificationAction)
}

ILClassificationAction 类型为:

/// Describes various classification actions.
@available(iOS 12.0, *)
public enum ILClassificationAction : Int, @unchecked Sendable {
   /// Indicate that no action is requested.
   case none = 0
   /// Report communication(s) as not junk.
   case reportNotJunk = 1
   /// Report communication(s) as junk.
   case reportJunk = 2
   /// Report communication(s) as junk and block the sender.
   case reportJunkAndBlockSender = 3
}

对于 ILClassificationAction.none,系统会关闭视图控制器,但不会采取任何其他操作。


对于 ILClassificationAction.reportNotJunkILClassificationAction.reportJunk,系统会根据 userInfo 属性生成报告,然后将其发布到扩展程序的 Info.plist 文件中指定的服务端(ILClassificationExtensionNetworkReportDestination)或者使用短信发到对应的号码(ILClassificationExtensionSMSReportDestination)。




对于 ILClassificationAction.reportJunkAndBlockSender,系统的响应就像在 ILClassificationAction.reportJunk 操作中一样。 但是,在报告步骤之后,系统会发出提示,让用户知道该号码将被阻止(拉黑)。


最后,为了保护用户隐私,系统会在 App Extension 终止后删除该容器。有关详细信息,请参阅关于 iOS 文件系统


参考资料




基于短信过滤能力。上线了喵喵消烦员 App:


喵喵消烦员是一款短信过滤工具软件。在如今信息爆炸的时代,您的隐私和安全由喵喵来守护!我们使用 scikit-learn,通过朴素贝叶斯算法对垃圾短信进行识别,通过 Core ML 将模型部署在本地,从而完成离线过滤任务。



  1. 隐私安全:我们不会索要任何位置、相机、通知、无线数据(网络)等权限,用户的数据不会被保存,同时也不可能被上传,所有操作均在本地完成。

  2. 高效精准:通过先进的算法技术,自动识别并过滤掉垃圾短信、诈骗信息、广告推销等不必要打扰的内容。

  3. 自定义设置:支持自定义关键词过滤,方便您针对个人需求进行个性化设置,避免不必要的干扰。

  4. 更新迭代:我们会通过版本升级定期更新过滤模型,确保始终能够准确地识别和拦截最新的垃圾短信和诈骗信息。


模型、App 还在不断调整和优化,欢迎使用和提供建议!




作者:Layer
链接:https://juejin.cn/post/7237697495109845052
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

程序员接外包的三个原则以及有意思的讨论

文章来源网络 原则一:乙方来做决策 最终拍板人是谁?是甲方,如果你非要抢板子,那你以后就没有甲方了但是,如果甲方也觉得“我花钱了,当然要听我的(那些只对上级负责又不能拍板的底层打工人,总是这样认为)”,那这种甲方的项目你就不要接因为在这种甲方眼里,你只是“施工...
继续阅读 »

文章来源网络


原则一:乙方来做决策


  • 最终拍板人是谁?是甲方,如果你非要抢板子,那你以后就没有甲方了
  • 但是,如果甲方也觉得“我花钱了,当然要听我的(那些只对上级负责又不能拍板的底层打工人,总是这样认为)”,那这种甲方的项目你就不要接
  • 因为在这种甲方眼里,你只是“施工方”,他们即不需要你的经验价值,更不会为你的经验买单。所以这种甲方你会做得很累,当他们觉得“你的工作强度不足以匹配付给你的费用时(他们总这样觉得)”,他们就会不停地向你提出新的开发需求
  • 所以,你要尽量找那种尊重你经验价值,总是向你请教,请你帮他做决策的甲方

原则二:为甲方护航


  • 甲方未来会遇到什么问题,你们双方其实都不知道,所以你需要一边开发一边解决甲方实际遇到的问题。
  • 因此,不要为了完成合同上的工作内容而工作,要为甲方遇到的实际问题,为了甲方的利益而开发,要提前做好变通的准备。
  • 永远不要觉得你把合同上的功能列表做完,你就能休息了。你要解决的真正问题是,为甲方护航,直至甲方可以自己航行。

原则三:不做没有用户的项目


  • 如果甲方的项目没有太多用户使用,这种项目就不要接。
  • 除了代码的累计经验,还有一种经验也很重要,那就是“了解用户的市场经验”
  • 只有真正面对有实际用户的项目,你才能有“解决市场提出的问题”的经验,而不是停留在“解决甲方提出的问题”
  • 拥有市场经验,你就会有更高的附加价值,再配上尊重你经验价值的甲方,你就会有更高的收入
  • 永远记住:真正愿意在你身上花钱的甲方,他的目的一定是为了让你帮他赚钱!

以上只是我根据自己经验的一家之言,可能对我有用,不一定对别人也有用。肯定还有很多有价值的原则,希望大家根据自己的经验一起来分享。


下面是一些有意思的讨论


原则 2 、3 都是虚的,就不讨论了。

只说原则一:


一般而言,甲方跟你对接的,一定不是老板。

所以他的核心目的一定是项目实施成功。

但项目是不是真的能给企业带来效益,其实是优先级特别低的一个选项。


拿日常生活举个例子。夫妻、情侣之间,你媳妇儿托你办个事,比如让你买个西瓜。

你明知道冬天的西瓜又贵又不好吃,你会怎么办?


A ,买西瓜,回去一边吃西瓜一起骂水果摊老板没良心。

B ,给你媳妇儿上农业课。然后媳妇儿让你跪搓衣板。

C ,水果摊老板训你一顿,以冬天吃白菜豆腐好为由,非卖你一颗大白菜。你家都不敢回。


这里面,你就是那个甲方对接人。你怎么选?


所以乙方一定不能做决策。乙方做决策的结果,就是甲方对接人被利益集团踹开或者得罪甲方对接人,最终导致项目失败




我也来说三个原则

1.要签合同,合同越细越好;

2.要给订金,订金越多越好;

3.尾款不结不给全部源码。




原则一:外包大部分就是苦力活,核心有价值的部分有自己公司的人干轮不到外包,你不干有的是人干,不会有人尊重你经验价值,甲方说怎么干就怎么干,写到合同里,按合同来,没甲方懂自家业务,别替甲方做决策,万一瞎建议导致项目出现大问题,黄了,外包钱都拿不回来


原则二:给多少钱办多少事,如果甲方给钱痛快,事少,可以看自己良心对甲方多上点心,否则别给自己加戏,不然很可能把自己感动了,甲方却想着好不容易碰上这么个人,白嫖


原则三:不做没有用户的项目,太片面,不是所有外包项目都是面对海量用户,但是做所有外包项目都是为了赚钱,假如有个富二代两三万找你做个毕设,简单钱多不用后续维护,这种接不接?假如某工厂几十万定制内部系统,可能只有几个人用,这种接不接


总之外包就是赚个辛苦钱,别指望这个来提升自己技术和自我价值,外包行业水太深,你这几个原则都太理想化




某富豪要盖一栋私人别墅,招建筑工人,现在缺一名搅拌水泥的工人,

找到了张三,张三说我去过很多工地,啥活儿都干过,经验极其丰富,我可以指导一切事物,我再给你兼职当个总设计师吧,一切事物听我的决策没错。

我每天做完我的本职工作搅拌水泥砂浆,我还能熬夜给建筑设计布局,风水,房间规划,材料采购等等,我啥都会,直接干到建筑完工

富豪很感兴趣,说那你来吧,我盖的是自己住的别墅,张三一听连连摆手:你是盖私人别墅啊?不行不行,我不去了,我以前盖的都是高楼大厦,住户多对我技术水平有严峻的考验,做成了对我有很大提高,私人别墅才几个人用,对我职业生涯一点帮助都没




永远不要接外包

这才是正确的答案

做私活的时间

不如自己休息休息,陪陪家人




屁事真多,有钱就行了,管他项目有没有人,人家产品低能你还得兜底,接外包考虑的是能不能满足需求。啥条件啊还能挑三拣四,给多少钱干多少活。 招投标接的 30 万以上的项目才有可能考虑你说的这些东西。





呵呵 我的意见是:

  1. 给钱就做(前提是合规,不是合理),先给定金,拿到定金开工。
  2. 遇到扯皮,就停止开发。
  3. 要有空闲的时间,偶尔做做(上面说的对:永远不要做外包)。

展开来说,做外包的长期收益很低。就当临时玩一下,所以给钱就做,不管你的需求合理不合理,比如甲方想给智障人士开发一款数独小游戏,好,给钱,签合同,支付定金,开工。


开工了3天,甲方突然说,那个我想加个魔方游戏。不好意思,不行,立即停止开发,开始和甲方掰扯,如果掰扯不明白,就终止合同,如果掰扯明白就继续。


不说了,我要和甲甲甲甲甲方掰扯去了。


作者:Data_Adventure
链接:https://juejin.cn/post/7256590619412676663
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

假如互联网人都很懂冒犯

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。 脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。 阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。 一步跨进电梯...
继续阅读 »

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。


脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。




阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。


一步跨进电梯间,我擦汗的动作凝固住了,挂上了矜持的微笑:“老板,早上好。”


老板:“早,你还在呢?又来带薪划水了?”


我:“嗨,我这再努力,最后不也就让你给我们多换几个嫂子嘛。”


老板:“没有哈哈,我开玩笑。”


我:“我也是,哈哈哈。”


今天的电梯似乎比往常慢了很多。


我:“老板最近在忙什么?”


老板:“昨天参加了一个峰会,马xx知道吧?他就坐我前边。”


我:“卧槽,真能装。没有,哈哈。”


老板:“哈哈哈”。


电梯到了,我俩都步履匆匆地进了公司。


小组内每天早上都有一个晨会,汇报工作进度和计划。


开了一会,转着椅子,划着朋友圈的我停了下来——到我了。


我:“昨天主要……今天计划……”


Leader:“你这不能说没有一点产出,也可以说一点产出都没有。其实,我对你是有一些失望的,原本今年绩效考评给你一个……”


我:“影响你合周报了是吗?不是哈哈。”


Leader、小组同事:“哈哈哈“。


Leader:“好了,我们这次顺便来对齐一下双月OKR,你们OKR都写的太保守了,一看就是能完成的,往大里吹啊。开玩笑哈哈。”。


我:”我以前就耕一亩田,现在把整个河北平原都给犁了。不是,哈哈。”


同事:“我要带公司打上月球,把你踢下来,我来当话事人。唉,哈哈”


Leader、同事、我:“哈哈哈“。


晨会开完,开始工作,产品经理拉我和和前端对需求。


产品经理:“你们程序员懂Java语言、Python语言、Go语言,就是不懂汉语言,真不想跟你们对需求。开个玩笑,哈哈。”


我:“没啥,你吹牛皮像狼,催进度像狗,做需求像羊,就这需求文档,还没擦屁股纸字多,没啥好对的。不是哈哈。”


产品经理、前端、我:“哈哈哈”。


产品经理:“那我们就对到这了,你们接着聊技术实现。”


前端:“没啥好聊的,后端大哥看着写吧,反正你们那破接口,套的比裹脚布还厚,没事还老出BUG。没有哈哈。”


我:“还不是为了兼容你们,一点动脑子的逻辑都不写,天天切图当然不出错。不是哈哈。”


前端、我:“哈哈哈”。


经过一番拉扯之后,我终于开始写代码了。


看到一段代码,我皱起了眉头,同事写的,我顺手写下了这样一段注释:

/**
* 写这段代码的人,建议在脑袋开个口,把水倒掉。不是哈哈,开个玩笑。
**/

代码写完了,准备上线,找同事给我Review,同事看了一会,给出了评论。



又在背着我们偷偷写烂代码了,建议改行。没有哈哈。



同事、我:“哈哈哈”。


终于下班了,路过门口,HR小姐姐还在加班。


我:“小姐姐怎么还没下班?别装了,老板都走了。开玩笑哈哈。”


HR小姐姐:“这不是看看怎么优化你们嘛,任务比较重。不是,哈哈。”


HR小姐姐、我:“哈哈哈”。


我感觉到一种不一样的氛围在公司慢慢弥散开来,我不知道怎么形容,但我想到了一句话——


“既分高下,也决生死”。




写这篇的时候,想到两年前,有个叫码农小说家的作者横空出世,写了一些生动活泼、灵气十足的段子,我也跟风写了两篇,这就是“荒腔走板”系列的来源。


后来,他结婚了。


看(抄)不到的我只能自己想,想破头也写不不来像样的段子,这个系列就不了了之,今天又偶尔来了灵感,写下一篇,也顺带缅怀一下光哥带来的快乐。


作者:三分恶
链接:https://juejin.cn/post/7259036373579350077
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

数组去重的多种方式

iOS
前言 从数组中删除重复项是一项常见的任务,在 Swift 中,标准库没有直接提供一个系统函数给我们,必须自己实现这样的方法。 实现数组去重的方法有很多,今天来介绍一些常用的方法。 1、使用 Set 去重 Set 也是一个集合,只是它不包含重复项,利用这个特点,...
继续阅读 »

前言


从数组中删除重复项是一项常见的任务,在 Swift 中,标准库没有直接提供一个系统函数给我们,必须自己实现这样的方法。


实现数组去重的方法有很多,今天来介绍一些常用的方法。


1、使用 Set 去重


Set 也是一个集合,只是它不包含重复项,利用这个特点,我们可以简单的给一个数组去重:

let array: [Int] = [1, 1, 3, 3, 2, 2]
let set: Set<Int> = Set(array)
print(set)


上边的代码会打印去重之后的 [1, 2, 3],但结果也可能是 [3, 1, 2],也可能是 [3, 2, 1],这就涉及到 Set 的原始设计了,它内部的元素是无序的,不能保证固定的顺序,如果你对顺序有要求,就不能用 Set 来实现了。


2、巧用字典去重


我们都知道字典中是无法存储相同 Key 的,也就可以利用 Key 的唯一性来去重:

let array: [Int] = [1, 1, 3, 3, 2, 2]
var dic: [Int: Int] = [:]
array.forEach { dic[$0] = 0 }
print(dic.keys)


先把 array 遍历一遍,所有元素作为字典的 key 存储起来,最后再取 dic.keys 获得去重之后的数组。


但是字典的 key 也一样是无序的,而且使用字典会带来额外的性能开销,因此不推荐这种方式。


上边提到这两种方案都无法保证顺序,如果需要保证去重后的顺序和原数组保持一致,请看下边的几个方案。


3、利用 NSOrderedSet


NSOrderedSet 是 OC 时代的产物,继承自 NSObject,它可以像 Set 一样实现去重,也可以保证顺序:

let array: [Int] = [1, 1, 3, 3, 2, 2]
let orderSet = NSOrderedSet(array: array)
print(orderSet.array)


最终打印 [1, 2, 3],顺序和原数组保持一致,但是需要注意这玩意性能比 Set 差很多。


4、遍历数组去重


这也是最符合直觉的方法,把数组遍历一遍,创建个新数组,如果这个元素没有加入新数组就加进去,如果加过了就抛掉:

let array: [Int] = [1, 1, 3, 3, 2, 2]
var newArray: [Int] = []
array.forEach { item in
    if !newArray.contains(item) {
        newArray.append(item)
    }
}
print(newArray)


最终打印 [1, 3, 2],也是保持顺序的。


为了更方便调用还可以将这个方法写个数组扩展:

extension Array where Element: Hashable {
    var unique: Self {
        var newArray: Self = []
        forEach { ele in
            if !newArray.contains(ele) {
                newArray.append(ele)
            }
        }
        return newArray
    }
}


这样调用的时候就方便了:

let array: [Int] = [1, 1, 3, 3, 2, 2]
print(array.unique) // [1, 3, 2]


5、filter 高阶函数 + Set


Set 在插入元素的时候调用 insert 函数,这个函数返回一个元组,第一个值是一个 Bool 类型代表是否插入成功,如果已经包含了这个元素则不能插入成功,另一个是被插入的这个元素,这在 Set 的函数声明中可以看得出来:

func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element)


我们可以利用这个特性,再加上 filter 这个高阶函数,来给集合 Array 写一个扩展:

extension Array where Element: Hashable {
    var unique: Self {
        var seen: Set<Element> = []
        return filter { seen.insert($0).inserted }
    }
}


调用方法和上边的方式一样。


作者:iOS新知
链接:https://juejin.cn/post/7275943600771399699
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

日志打得好,代码差不了

1. 前言 众所周知,一个完善的日志体系对于开发的顺利进行至关重要。尽管构建这样一个系统可能需要与开发几个功能模块相当的时间,但为了在日常开发中高效地记录日志,我进行了以下尝试。希望这些经验能为后端入门的同学和对日志管理不够熟悉的朋友们提供帮助。 (本文面向后...
继续阅读 »

1. 前言


众所周知,一个完善的日志体系对于开发的顺利进行至关重要。尽管构建这样一个系统可能需要与开发几个功能模块相当的时间,但为了在日常开发中高效地记录日志,我进行了以下尝试。希望这些经验能为后端入门的同学和对日志管理不够熟悉的朋友们提供帮助。


(本文面向后端入门同学或是对日志管理缺乏深入了解的朋友)


2. 注解+手动记录


使用注解@Sl4j来自动创建日志类,再通过log.info()手动记录日志,是一种常见且相对灵活的方法。这样,我们可以在需要的任何地方记录所想要的日志内容。


然而,在实际开发中,这种方法可能会导致大量的日志与业务逻辑代码混杂,增加了代码与日志的耦合度。当需要修改业务逻辑时,往往还需要调整相关的日志,这可能导致重复记录,如请求参数和鉴权信息。


尽管如此,我并不是完全反对这种日志处理方法。其简单性和灵活性是其显著优点。但建议开发者结合其他方法,以减少日志与业务逻辑的耦合。


3. AOP统一处理


首先,我会介绍如何利用AOP自动记录日志以及哪些日志适合用AOP来处理。


3.1 请求详情日志

使用AOP统一处理请求详情日志能够有效地解耦控制层和业务层,这是一个非常高效的策略。以下是一个示例,展示如何使用AOP通知类记录详细的请求信息:


@Slf4j
@Aspect
@Component
public class LoggingAspect {

/* 日志输出被访问的接口url */
@Before("execution(* com.steadon.example.controller.*.*(..))")
public void beforeRequest(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

String httpMethod = request.getMethod();
String remoteAddr = request.getRemoteAddr();
String requestURI = request.getRequestURI();
String queryString = request.getQueryString();
String params = "";

if ("POST".equalsIgnoreCase(httpMethod)) {
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
params = Arrays.toString(args);
}
}

log.info("Request Info: IP [{}] HTTP_METHOD [{}] URL [{}] QUERY_STRING [{}] PARAMS [{}]",
remoteAddr, httpMethod, requestURI, queryString, params);
}
}

通过这个AOP通知类,我们可以轻松地记录详细的请求信息。在遇到问题时,分析此日志通常可以帮助我们迅速找到原因,例如前端参数传递错误或后端参数接收问题。效果图如下:


image.png


3.2 其他

你可以利用AOP来拦截几乎任何切点,无论是消息队列、Mybatis的Mapper操作,还是特定的循环,都可以织入相应的通知。然而,使用AOP时,我们也必须权衡其成本。


AOP的优势并不仅仅在于其性能或易用性,而是它出色的解耦能力。选择是否使用AOP应基于你的日志和业务逻辑之间的耦合程度。只有当耦合度足够高,需要解耦时,AOP才真正显示其价值。


4. Lark机器人 + 全局异常处理


我相信许多读者都已经熟悉飞书机器人(Lark机器人)。通过全局异常捕获并调用飞书机器人进行告警,这是一种极为有效的业务告警通知方式。你可能会想:“哦,这个我知道。” 但是,具体如何实现呢?其实操作起来并不复杂,接下来我会详细介绍:


4.1 创建告警群聊

对于简单的报警,我们通常选择创建群聊机器人,而不是单独的机器人应用。这样做的好处是,我们可以动态管理需要接收告警的开发团队成员。


4.2 创建自定义机器人


  1. 在群聊中,依次选择:设置 -> 群机器人 -> 添加机器人。


image.png



  1. 在机器人详情页面,获取webhook地址。请确保此地址保密。


image.png



  1. 接下来,在代码中集成飞书机器人的告警功能。下面是如何在全局异常处理中集成飞书机器人的示例代码:


/* 飞书机器人告警 */
public String sendLarkNotification(String webhookUrl, String user, String title, String messageBody) throws Exception {
URL url = new URL(webhookUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");

ObjectMapper mapper = new ObjectMapper();
ObjectNode root = mapper.createObjectNode();
ObjectNode content = root.putObject("content").putObject("post").putObject("zh_cn");
content.put("title", title);

ArrayNode contentArray = content.putArray("content");
ArrayNode atUserArray = contentArray.addArray();
atUserArray.addObject().put("tag", "at").put("user_id", user);

ArrayNode messageArray = contentArray.addArray();
messageArray.addObject().put("tag", "text").put("text", messageBody);

root.put("msg_type", "post");

byte[] input = mapper.writeValueAsBytes(root);

try (OutputStream os = connection.getOutputStream()) {
os.write(input, 0, input.length);
}

try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
response.append(line);
}
return response.toString();
}
}

代码相对简单,主要逻辑是通过HTTP请求向webhook地址发送带有告警信息的POST请求。为了实现这个请求,我们需要在全局异常处理中调用上述方法。为了便于展示,我直接在全局异常处理类中编写了这个方法。但读者可以考虑创建一个专门的工具类来实现这一功能。接下来,我将展示我的全局异常处理类的逻辑:


@ControllerAdvice
public class GlobalExceptionHandler {

@Value("${notifications.larkBotEnabled}")
private boolean larkBotEnabled;

@ExceptionHandler(value = Exception.class)
public CommonResult<String> handleException(Exception e) {
if (!larkBotEnabled) return CommonResult.fail();
// Send notification to Lark
String title = "线上BUG通报";
String user = "all";
String webhookUrl = "https://open.feishu.cn/open-apis/bot/v2/hook/f4150b6c-xxxx-xxxx-xxxx-xxxx-xxxx";

try {
String s = sendLarkNotification(webhookUrl, user, title, e.getMessage());
return CommonResult.fail(s);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}

你可能会对larkBotEnabled感到好奇。实际上,全局异常捕获并不会自动区分线上环境与开发环境。为了能够在不同的环境中控制是否发送告警,我们使用了这个变量。通过在线上和本地的配置文件中设置不同的值,我们可以轻松地区分这两种环境。


application.yml


spring:
profiles:
active: dev #决定是否使用本地配置
application:
name: app-name

notifications:
larkBotEnabled: true #自定义字段控制报警行为

application-dev.yml


notifications:
larkBotEnabled: false

这样,我们就可以根据不同的环境来决定是否发送告警。以下是告警效果的示例:


image.png


这种告警方式既简洁又直观,帮助我们迅速定位问题。当然,针对不同的问题,我们还可以进一步对告警进行分级处理。


总结:在大型项目中,日志体系的构建需要大量的时间和资源。虽然在实际业务中,我们可能会使用更复杂的日志系统和告警机制,但本文提供的方法对于日常开发已经足够使用。如有任何建议或纠正,欢迎在评论区提出!


作者:雾山小落
来源:juejin.cn/post/7280864416554500131
收起阅读 »

某律师执业诚信信息公示平台字体加密解决思路

web
本文章只做技术探讨,请勿用于非法用途。 目标网站 为持续加深对律法的学习, 我们需要再来收集一些数据。 本文来解决 credit.acla.org.cn/ 这个网站的字体加密问题。 网站分析 网站反爬 这个网站的各种反爬措施还挺多的, 接口加密啊, 验证码啊...
继续阅读 »

本文章只做技术探讨,请勿用于非法用途。



目标网站


为持续加深对律法的学习, 我们需要再来收集一些数据。


本文来解决 credit.acla.org.cn/ 这个网站的字体加密问题。


网站分析


网站反爬


这个网站的各种反爬措施还挺多的, 接口加密啊, 验证码啊(每个页面都有), 无限 debugger 啊, 什么的, 还是挺烦的, 如果不要求效率的话可以考虑用 selenium 来过掉, 这里重点来解决一下字体加密的问题。


加密分析


首先来看下字体加密什么样子。


image.png


如图, 为律所详情页的截图, 可以看到啊, 这个 标签下的字体为加密字体, 这个网站他大多数数据信息都会像这样来做一个加密。


开整


首先来说下解决的方法。




  1. 找到字体文件。




  2. 确定文件字体与网站字体的映射关系。




  3. 替换网站字体。




字体文件获取


image.png


刷新页面, 勾选字体栏即可看到返回的页面, 直接下载下来即可。



有些网站可能会返回多个字体文件来迷惑你, 这时候可以全局搜索 ttf 等字体文件的关键词, 来读相关代码来找到前端页面解密时用的具体是字体文件。



字体文件解析


字体文件处理可以用 TTFont 工具, 我们先将文件解析成可读的 xml 格式来看下这到底是什么个东西。


image.png


下载字体文件保存为本地 font.ttf, 然后解析为 font.xml。


image.png


可以看到文件里是一些映射关系, 和一些字形的信息, 如果是简单的数字加密或是很少的字体加密的话, 这一步直接拿到映射关系就可以用了, 但是这个网站他每次的字体文件都不一样, 所以这种简单的映射关系不可用。


image.png


在字体编辑软件里也可以看到是对哪些字体进行了修改加密, windows 可以用 font creator , mac 上我用的是 FontForge 来解析的。


映射关系获取


上一步我们拿到了字形信息, 这里来生成提供一个通用的方法来做映射关系。



font.ttf 文件中通过 unicode 码来标记对应的字体的字形信息, 我们也可以用同样的方式, 获取加密字体对应的原字体的字形信息(固定不变的), 以此为 key 来设计映射关系。



image.png


这里定义了一个全局变量 font_map 来存储映射关系, 通过 PIL 的 ImageFont 对象来将字体的字形信息复现出来, 然后通过 ocr 技术得到字的原型, 完成解密。将解密过的字存入 font_map, 随着收录的字越来越多, 解析效率会越来越高。


加密字体替换


image.png


这里没什么难度, 做一个简单的替换就好。


结语


这个也是我首次接触这种麻烦些的字体加密, 就想写出来权当分享, 思路也是借鉴于之前看到的一个帖子(找不见了。。)。 文中有些东西需要自己去调试后可能会理解更深些, 因为写的过程中被其他事情打断了几次, 之前整理的思路乱掉了, 写的可能不太顺畅, 大家哪里不懂的话可以留言讨论吧, 或者有什么更好的思路也欢迎来交流。


作者:Glommer
来源:juejin.cn/post/7272399042091909131
收起阅读 »

关于我的人生(假如我工作13年就能得到3w个花西币)

关于我的人生(假如我工作13年就能得到3w个花西币) 前言 我一直在思考一个问题,我们真的有正视过未来吗? 其实大家可能会遇到一种可能就是平平淡淡的过完一生,什么20岁CEO、年纪轻轻福布斯前几?这种小说中主角的模板可能不一定会出现在我们身上。(这里并不是嘲笑...
继续阅读 »

关于我的人生(假如我工作13年就能得到3w个花西币)


前言


我一直在思考一个问题,我们真的有正视过未来吗?


其实大家可能会遇到一种可能就是平平淡淡的过完一生,什么20岁CEO、年纪轻轻福布斯前几?这种小说中主角的模板可能不一定会出现在我们身上。(这里并不是嘲笑,谁不想拥有爽文男主的人生,笔者还是希望大家能过得很好)


这段时间我正处于离职状态就一直在思考,我究竟是在做什么?


毕业了两年,在一家公司做了两年拿了一笔不算多不算少的薪水,然后每天不知道为何一直忙碌着,这也许就是大部分人的现状。


image.png


image.png



有很多人一生都不知道,自己要做什么?



这几天我重新看到这句话,我就感觉说得真的很对


那么就会有人说,我一生要做的事情就是搞钱,没错很多人都是如此。


image.png


关于我




  • 目前做前端开发,月薪9k,2年工作经验,工作中算不算特别出彩,但是也有一定能力,简单说普通




  • 学历: 本科(非211、985)




  • 性格: 偏内向,朋友较少,交际一般




  • 爱好: 游戏、跑步、健身




  • 不良爱好: 无




预景


目前,25岁,假设工作到38岁10年时间薪资预计如下



  • 3-5年工作经验 11-14k 继续工作3年 一年按照13.2w算 三年39.6万

  • 5-10年工作经验 14-18k 继续工作5年 一年16.w算 5年84万

  • 10-15年工作经验 20-22k 继续工作5年 一年24w 5年 120万


预计会获得243.4w ,13年时间,当前这是目前比较客观并且没有考虑环境通货膨胀等其他元素的情况下,并且涨薪也是比较优异的情况下,不吃不喝工作13年获得的理想薪资。



是不是看着挺多,哇200多万呢,换算成花西币能有3w枚



那么按照我目前工作的地点厦门,我不吃不喝13年,可能连房子都买不起


副业的可能


副业:这个东西不一定大家都适合,其实我感说许多人都想过,但是大家真的挣到钱了吗?


我这里可能可以直接给出答案没有,为什么?



我自己就是一个实例:



上面介绍了我是一个程序员,上班时间是9:00~18:00通常是朝9晚6,如果项目赶还需要加班,那么我的副业,肯定需要一点时间灵活,那么就限制了很多,固定时间工作我都做不了


image.png



那么这样说大家就会想到几个选项




  • 自媒体

  • 滴滴

  • 骑手

  • 接私活

  • 摆摊


这是我目前能想到几个,我先逐个分析



自媒体



目前我有尝试过,那么问题来了我播什么?才艺没有颜值,额虽然是吴彦祖级别,身材是彭于晏,大家v我50,我可以继续吹下去


image-20230917135313799


不过我认为:做自媒体依然是最佳的选择,我现在虽然没有什么可以播的东西,但是我依然在寻找一个自己适合的方向,我个人认为内容应该有具备以下特点



  • 自己一直在坚持做的东西,例如: 我喜欢吃美食、经常会探店,这样你才有持续的内容输出

  • 稀缺的东西,例如:网上很多探店自助、米其林、高端,说白了就是平常少接触的东西

  • 能力主播(搞笑、颜值、特长、跳舞等等),这不得不说颜值也是一种优势,这个大家都懂


暂时就想到这么多,目前虽然做自媒体的人很多,但是依然有很多机会。


我自己也在寻求机会和方向



滴滴、骑手



这个两个我就一起说了仁者见仁,智者见智,首先个人肯定不推荐,你上班一天做了8小时,晚上下班继续跑滴滴、送外卖,说实话特别苦,我之前就跑了一段时间,收入非常惨淡,同时收入不具备发展性


我之前下班6.30左右,吃完饭7点左右去送外卖10点左右回家,可能3小时才30左右,一单4块一般,而且单子少,人家5点多-7点是外卖最多的时候,你这时候也是刚才下班去吃完饭,7-8都是晚吃饭。



时间冲突,并且收入不可观,如果是娱乐逛逛风景可以跑着玩



下面附上我送外卖的收入,可以说一晚上2 3小时最多收入30-40左右一天


image-20230917140243436



摆摊



摆摊:如果是我可能不考虑,周末可以一试,但是周一到周五,可能自己没办法摆摊


原因:


摆摊一般都是卖吃的6-7点下班。


假设材料你都以前准备好了,回家出摊7点左右,这时候饭点都差不多过了,怎么可能卖得出去,就算不是卖吃的下班人都回家,客流量小了,肯定会印象收益



接私活



接私活这一项我自己感觉太难了,说实话,除非是公司不忙的人,可以考虑。


例如: 我上班一天8小时,都在面对电脑,下班了还得继续干,说实话,我很难坚持,也许是我比较弱鸡,我自认为自己毅力还是有的,即使这样我自己也不太会去考虑私活


我认识的有些大佬是真的强,不过也许有些也是上班不忙的时候写,具体还是看自己的情况。


结论


目前环境真的很不好,互联网现在已经开始饱和了,后面失业的人可能会越来越多,同时环境也会越来越差,想到在这环境下面生存真的很不容易。加油!!!都看到这里不妨点个赞!



作者:柒丶月
来源:juejin.cn/post/7280747833384484919
收起阅读 »

Kotlin Flow入门

Flow作为Android开发中的重要的作用。尤其在Jetpack Compose里左一个collect,右一个collect。不交接Flow而开发Android是寸步难行。作为一个入门文章,如果你还不是很了解Flow的话,本文可以带你更进一步的了解Flow。...
继续阅读 »

Flow作为Android开发中的重要的作用。尤其在Jetpack Compose里左一个collect,右一个collect。不交接Flow而开发Android是寸步难行。作为一个入门文章,如果你还不是很了解Flow的话,本文可以带你更进一步的了解Flow。


Flow是一个异步数据流,它会发出数据给收集者,最终带或者不带异常的完成任务。下面我们通过例子来学习。


假设我们正在下载一幅图片。在下载的时候,还要把下载的百分比作为值发出来,比如:1%,2%,3%,等。收集者(collector)会接收到这些值并在界面上以合适的方式显示出来。但是如果出现网络问题,任务也会因此终止。


现在我们来看一下Flow里的几个API:



  • 流构建器(Flow builder)

  • 操作符(Operator)

  • 收集器(Collector)


流构建器


简单来说,它会执行一个任务并把值发出来,有时也会只发出值而不会执行什么任务。比如简单的发出一些数字值。你可以把流构建器当做一个发言人。这个发言人会思考(做任务)和说(发出值).


操作符


操作符可以帮助转化数据。


我们可以把操作符当做是一个翻译。一个发言人说了法语,但是听众(收集器)只能听懂英语。这就需要一个翻译来帮忙了。它可以把法语都翻译成英语让听众理解。


当然,操作符可以做的远不止这些。以上的例子只是帮助理解。


收集器


Flow发出的值经过操作符的处理之后会被收集器收集。


收集器可以当做是收听者。实际上收集器也是一种操作符,它有时被称作终端操作符


第一个例子


flow { 
(0..10).forEach {
emit(it)
}
}.map {
it * it
}.collect {
Log.d(TAG, it.toString())
}

flow {}->流构建器
map {}->操作符
collect {}->收集器

我们来过一下上面的代码:



  • 首先,流构建器会发出从0到10的值

  • 之后,一个map操作符会把每个值计算(it * it)

  • 之后,收集器收集这些发出来的值并打印出来:0,1,4,9,16,25,36,49,64,81,100.


注意:collect方法把流构建器和收集器连到了一起,这个方法调用之后流就开始执行了。


流构建器的不同类型


流构建器有四种:



  1. flowOf():从一个给定的数据集合生成流

  2. asFlow(): 一个扩展方法,可以把某个类型转化成流

  3. flow{}: 我们例子中使用的方法

  4. channelFlow{}:使用构造器自带的send方法发送的元素构建流


例如:


flowOf()


flowOf(4, 2, 5, 1, 7) 
.collect {
Log.d(TAG, it.toString())
}

asFlow()


(1..5).asFlow()
.collect {
Log.d(TAG, it.toString())
}

flow{}


flow {
(0..10).forEach {
emit(it)
}
}
.collect {
Log.d(TAG, it.toString())
}

channelFlow{}


channelFlow {
(0..10).forEach {
send(it)
}
}
.collect {
Log.d(TAG, it.toString())
}

flowOn操作符


flowOn这个操作符可以控制flow任务执行的线程的类型。在Android里一般是在一个后台线程执行任务,之后在界面上更新结果。


下面的例子里加了一个500毫秒的延迟来模拟实际任务。


val flow = flow {
// Run on Background Thread (Dispatchers.Default)
(0..10).forEach {
// emit items with 500 milliseconds delay
delay(500)
emit(it)
}
}
.flowOn(Dispatchers.Default)

CoroutineScope(Dispatchers.Main).launch {
flow.collect {
// Run on Main Thread (Dispatchers.Main)
Log.d(TAG, it.toString())
}
}

本例,流的任务就会在Dispatchers.Default这个“线程”里执行。接下来就是要在UI线程里更新UI了。为了做到这一点就需要在UI线程里collect


flowOn操作符就是用来控制任务执行的线程的。它的作用和RxJava的subscribeOn类似。


Dispatchers主要有这些类型:IODefaultMain。flowOn和CoroutineScope都可以使用Dispatchers来执行任务执行的“线程”(暂且这么理解)。


使用流构造器


我们通过几个例子学习。


移动文件


这里我们用流构造器新建一个流,让流任务在后台线程执行。完成后在UI线程显示状态。


val moveFileflow = flow {
// move file on background thread
FileUtils.move(source, destination)
emit("Done")
}
.flowOn(Dispatchers.IO)

CoroutineScope(Dispatchers.Main).launch {
moveFileflow.collect {
// when it is done
}
}

下载图片


这个例子构造一个流在后台线程下载图片,并且不断的在UI线程更新下载的百分比。


val downloadImageflow = flow {
// start downloading
// send progress
emit(10)
// downloading...
// ......
// send progress
emit(75)
// downloading...
// ......
// send progress
emit(100)
}
.flowOn(Dispatchers.IO)

CoroutineScope(Dispatchers.Main).launch {
downloadImageflow.collect {
// we will get the progress here
}
}

现在你对kotlin的流也有初步的了解了,在项目中可以使用简单的流来处理异步任务。


什么是终端操作符


上文已经提到过collect()方法是一个终端操作符。所谓的终端操作符就是让流跑起来的挂起方法(suspend function)。在以上的例子中,流构造器构造出来的流是不动的,让这个流动起来的操作符就是终端操作符。比如collect


还有:



  • 转化为各种集合的,toList, toSet

  • 获取第一个first,与确保流发射单个值的操作符single

  • 使用reduce, fold这类的把流的值规约到单个值的操作符。


比如:


val sum = (1..5).asFlow()
.map { it * it } // 数字 1 至 5 的平方
.reduce { a, b -> a + b } // 求和(末端操作符)
println(sum)

冷热流


前面的例子里的流都是冷流。我们来对比一下流的不同:


冷流热流
收集器调用的时候开始发出值没有收集器也会发出值
不存储数据可以存储数据
不支持多个收集器可以支持多个收集器

冷流,如果带上了多个收集器,流会每次遇到一个收集器就从头把完整的数据发送一次。


热流遇到多个收集器的时候,流会一直发出数据,收集器开始收集数据的时候遇到的是什么数据就收集什么数据。热流的多个收集器共享一份数据。


冷流是推模式,热流是拉模式。


下面看几个例子:


冷流实例


fun getNumbersColdFlow(): ColdFlow<Int> {
return someColdflow {
(1..5).forEach {
delay(1000)
emit(it)
}
}
}

开始收集


val numbersColdFlow = getNumbersColdFlow()

numbersColdFlow
.collect {
println("1st Collector: $it")
}

delay(2500)

numbersColdFlow
.collect {
println("2nd Collector: $it")
}

输出:


1st Collector: 1
1st Collector: 2
1st Collector: 3
1st Collector: 4
1st Collector: 5

2nd Collector: 1
2nd Collector: 2
2nd Collector: 3
2nd Collector: 4
2nd Collector: 5

两个收集器都从头获取到流的数据,在每次收集的时候都相当于遇到了一个全新的流。


热流实例。本例会设置一个热流每隔一秒发出一个1到5的数值。


fun getNumbersHotFlow(): HotFlow<Int> {
return someHotflow {
(1..5).forEach {
delay(1000)
emit(it)
}
}
}

现在开始收集:


val numbersHotFlow = getNumbersHotFlow()

numbersHotFlow
.collect {
println("1st Collector: $it")
}

delay(2500)

numbersHotFlow
.collect {
println("2nd Collector: $it")
}

输出:


1st Collector: 1
1st Collector: 2
1st Collector: 3
1st Collector: 4
1st Collector: 5

2nd Collector: 3
2nd Collector: 4
2nd Collector: 5

StateFlow


在Android开发中,热流的一个很重要的应用就是StateFlow


StateFlow是一种特殊的热流,它可以允许多个订阅者。如果你使用了jetpack compose来开发app的话,StateFlow可以简单而高效的在app的不同地方享状态(state)。因为热流只发送当前的状态(而不像冷流那样从开始发送值)。


要新建一个StateFlow,可以使用MutableStateFlow,然后给它一个初始值:


val count = MutableStateFlow(0)

在这里新建了一个叫做count的StateFlow,初始值为0。要更新它的值可以使用update方法,或者value属性:


this.count.update { v -> v + 1 }
this.count.value = 10

这时,订阅了count状态的订阅者就可以收到更新之后的值了。要订阅可以这样:


count.collect {
//...
}

在冷热流之外还有两种流:回调流和通道流。这个后面会详细讲到。


SharedFlow


SharedFlow也是一种热流,主要用于事件流。它会对所有的活的收集器发送事件。不同的消费者可以在同一时间收到同一个事件。


可以使用MutableSharedFlow()方法来创建一个SharedFlow对象。可以通过replay参数指明多少个已经发送的事件可以再发送给新的收集器,默认的是0。也即是在默认情况下,收集器只会接收到开始收集之后发送过来的事件。


这个时候可以来一个例子了:


class TickHandler(
    private val externalScope: CoroutineScope,
    private val tickIntervalMs: Long = 5000
) {
    // Backing property to avoid flow emissions from other classes
    private val _tickFlow = MutableSharedFlow<Unit>(replay = 0) // 1
    val tickFlow: SharedFlow<Event<String>> = _tickFlow // 2

    init {
        externalScope.launch {
            while(true) {
                _tickFlow.emit(Unit) // 3
                delay(tickIntervalMs)
            }
        }
    }
}

class NewsRepository(
    ...,
    private val tickHandler: TickHandler, // 4
    private val externalScope: CoroutineScope
) {
    init {
        externalScope.launch {
            // Listen for tick updates
            tickHandler.tickFlow.collect { // 5
                refreshLatestNews()
            }
        }
    }

    suspend fun refreshLatestNews() { ... }
    ...
}

示例解析:



  1. MutableSharedFlow声明了一个变量_tickFlow

  2. 定义了属性tickFlow

  3. 在初始化的时候使用SharedFlow成员变量_tickFlow每隔一段时间发送一个空事件

  4. NewsRepository类里声明成员变量tickHandler

  5. NewsRepository初始化之后开始收集事件,并在收集到事件之后调用refreshLatestNews方法来更新新闻。


看完这个例子再结合上面的介绍就会更加深入的了解SharedFlow了。


注意



  • 这SharedFlow是用于事件流处理的,可不是用来维护状态(state)的。

  • SharedFlow的另外一个重要的参数是extraBufferCapacity,它决定了流要在缓存里保留多少个发送过的事件。缓存满了之后会把缓存里面的一个值清理掉,并放入新的值。

  • 要处理缓存溢出的问题可以给onBufferOverflow指定一个方法。比如当缓存满了之后,并遇到新的事件的时候清理掉最旧的值或者暂停发送新事件一直到缓存有空余。

  • 可以使用tryEmit方法来检测是否存在一个活的收集器。这样可以避免无效的事件发送。


热流的坑


如果在同一个协成里订阅了多个热流,只有第一个才会被收集。其他的永远不会得到数据。


所以,要在同一个协成里订阅多个热流可以使用combine或者zip操作符把这些热流都合成到同一个流里。或者分别在每个协程订阅一个热流。


例如:


coroutineScope.launch {
hotFlow1.collect { value ->
// 处理收到的数据
}
hotFlow2.collect { value ->
// 永远不会执行到
}
}

在本例中,第二个collect不会收到数据。因为第一个collect会运行一个无限循环。


背压 (Backpressure)


背压,顾名思义,当消费者消费的速度没有生产者生产的速度快了。在Flow遇到这个情况的时候,生产者就会挂起直到消费者可以消费更多的数据。


runBlocking {
getFastFlow().collect { value ->
delay(1000) // simulate a slow collector
process(value)
}
}

在这个例子中,getFastFlow()会生成数据的速度比process(value)的速度快。因为collect是一个挂起函数,在process(value)数据处理不过来的时候getFastFlow()就会自动挂起。这样就防止了没有处理的数据的堆积。


使用缓存处理背压


有的时候,即使消费者处理速度已经慢于生产者产生数据的速度的时候,你还是想让生产者继续生产数据。这时就可以引入缓存了。Flow可以使用buffer操作符。如:


runBlocking {
getFastFlow().buffer().collect { value -> process(value) }
}

这个例子里使用了buffer操作符,这样在process(value)还在处理旧数据的时候getFastFlow()可以接着生产新的数据。


今日份先更新到这了。to be continued...


作者:小红星闪啊闪
来源:juejin.cn/post/7271153372793946168
收起阅读 »

打工:本身就毫无意义

昨天的时候有一个朋友联系我,说:“他被裁员了,一个工作了 11 年的老 JAVA” 其实在这段时间里面,我收到了很多类似的朋友来信:被裁员,焦虑到睡不着觉。 甚至有些公司,明确的表示不会给你 n+1 。 这些事,让我深切的感受到什么叫做:取之尽锱铢,用之...
继续阅读 »

昨天的时候有一个朋友联系我,说:“他被裁员了,一个工作了 11 年的老 JAVA”



其实在这段时间里面,我收到了很多类似的朋友来信:被裁员,焦虑到睡不着觉。



甚至有些公司,明确的表示不会给你 n+1 。



这些事,让我深切的感受到什么叫做:取之尽锱铢,用之如泥沙,弃之是垃圾。


那么对于我们这些普通的打工人而言,如何应对这样的一个时代,如何才能让自己更加具备竞争力,不要陷入毫无意义的打工内卷呢?


这个也就是咱们这次,主要说明的问题。


一共分为三点:



  1. 要工作,不要打工

  2. 如何得分,为自己工作

  3. 什么时候应该选择打工


要工作,不要打工


工作和打工是不一样的。


想要让自己真正的成长,首先我们需要区分出工作和打工的区别。


打工


回忆一下你的打工生涯,你有没有为今天摸鱼一天,但是工资照发而感到高兴。有没有为今天加班到深夜,但是一点加班费都没有,而感到愤怒。


如果你有这样的情绪,那么就是没有搞明白打工的本质的原因。


打工本质上就是一个机器,它有两个口 一个口吸收你的时间,一个口产出对应的金钱。


产出的金钱多少,与你的工作量,并无本质关系。它需要消耗的是你的时间。


现在很多公司都在要求 996、大小周。很多朋友都说:“ 996 有什么用啊,工作效率那么低,我们只需要在规定的时间之内,把事情做完不就可以了吗?不知道哪个王八蛋想出来的这个。公司也是傻X!”


你以为:公 司 不 知 道 9 9 6 效 率 低 吗? 。你可以怀疑某些公司的人品,但不要怀疑他们的智商(注意:我说的是某些公司,我相信大部分公司是好的~~)


那么既然公司知道 996 效率低,为什么还要让你 996 呢?


说白了就是尽量压榨你的时间。让你没有时间再去思考工作真正的意义。


工作


接下来,我们来说工作。


之前在群里看到了一个朋友说的话,我觉得非常准确,在这里分享给大家:



如果把一个公司比喻成一个篮球队。那么在这个球队里面,将会有两个角色,一个叫做 投篮手
,一个叫做 其他


公司的老板担任的就是投篮手的角色,公司的员工担任的就是其他的角色。


其他的角色的主要任务就是帮助投篮手得分。所以,你运球再好,传球再好,都没有关系。


但是,得分必须要投篮手完成,其他是不允许得分的,也不允许练习得分,甚至连得分的想法都不应该有。



打工压榨你的时间,不允许你做私活,不允许你做任何可以自己创造利润的事情。说白了就是让你尽可能的思考如何运球、如何传球,但是不允许思考如何得分。


而工作区别于打工的地方,就是:工作你要思考的问题就是:如何得分!


很多开发者都是 唯技术至上论,我们永远在思考如何利用技术来换取更高的薪酬,但是我们没有思考过如何利用技术来自己得分。


所以说,当我们想要把打工思维,转化为工作思维的时候。首先需要做的就是:如何可以利用自己技术,来创造出直接属于自己的利润。


这个利润一定不是你通过打工机器换来的,而是通过自己得分得来的。


当有一天,你发现:原来我也是可以投篮的。到这个时候,你就算真正的知道:如何工作了。


如何得分,为自己工作


那么到现在咱们知道了工作和打工是不一样,工作更关注如何投篮。


那么接下来,咱们就来说一说,投篮这个事怎么做呢?


所谓投篮,就是:通过自己现有的储备,产生直接价值,利用对应的价值换取收益


其中,最简单的方式就是:利用现有平台,产生延伸价值


利用现有平台,产生延伸价值


打工机器最好的一点就是:他可以让你在对市场一无所知的时候,快速的进入到这个市场中。从而让你可以知道这个市场运行的规律。


只要你有打过工,那么你一定可以接触到公司内部的一些业务。那么好好的利用这个机会,想一下:“这些业务究竟是解决了市场上哪些现有的问题?目前还有哪些不足?我是否做一些事情,弥补这些不足。”


以我为例:


我之前在黑马工作,从而接触到了线下培训的业务。发现了线下培训的三个问题:



  1. 线下培训,费用极高,通常需要在两万元以上。额外学生还需要支付住宿费、生活费等其他费用。六个月下来,每个学员支出需要在 4-5万元 之间。

  2. 老师的水平参差不齐,运气好撞见了一个好老师(比如像我这样的),可以学的比较好。如果点背,碰上了一个没有那么负责的老师,那么学习情况真的堪忧。

  3. 统一授课需要保证统一进度,所以每天要讲的内容都是固定的。你进度慢跟不上这个进度,那么不好意思。这些都是你的问题,你要想办法解决。课程不会等你。


所以,针对这三个问题,我就在想,那么是不是可以做线上的培训?这样有三点好处:



  1. 费用低:因为没有线下教室的成本,所以可以把费用压到很低。

  2. 1V1私教:我全程提供 1V1 的私教,手把手教学、解决问题。可以保证每个同学都可以得到一个好老师。

  3. 定制化教学:不统一授课,每个同学学习进度都可以不一样。根据每个同学的学习情况进行定制化的教学。


以此来解决线下培训中存在的三个问题。


这就属于:“发现现有业务的不足,从而通过一些改变,来弥补这些不足”。从而开始尝试自己投篮。


什么时候应该选择打工


那么说到这里,打工就毫无意义了吗?我们就应该直接开始学习投篮吗?


当然不是!


所以最后,咱们就来说一下,什么时候应该选择打工。


当我们刚刚走出校园,或者虽然已经工作,但是还没有发现现有业务的一些不足时。


那么打工是一个最好的选择。


千万不要贸然的放弃现有的收入!


但是,我们需要知道的是:打工的目的绝不仅仅只是为了通过时间换取金钱。而一定是,利用现有的平台,发现它所存在的问题,然后想办法解决这个问题。


也就是:打工的终极形态,就是可以真正的工作!


而:工作的终极形态,就是发现了一个社会中现有的问题,然后想办法解决它!


作者:程序员Sunday
来源:juejin.cn/post/7280104452399743028
收起阅读 »

各位微信小程序的开发者们,你们还好吗?

web
前言 最近微信小程序隐私指引这波骚操作实在是太好玩了,剧情跌宕起伏,让人不由得直呼周星驰在《唐伯虎点秋香》里的那句名言“人生大起大落得太快,实在是太刺激了”。 我自己因为有几个小程序需要做适配,所以全程体验了整个剧情,到今天感觉这过程实在是有点离谱,所以决定...
继续阅读 »

前言


最近微信小程序隐私指引这波骚操作实在是太好玩了,剧情跌宕起伏,让人不由得直呼周星驰在《唐伯虎点秋香》里的那句名言“人生大起大落得太快,实在是太刺激了”。


3583c74475ac4036932cf1421e1abafd.jpeg


我自己因为有几个小程序需要做适配,所以全程体验了整个剧情,到今天感觉这过程实在是有点离谱,所以决定记录一下。以飨读者。


起因


让我们把时钟回拨到一个月前的8月14日,微信默默的发了一个足以影响所有小程序的公告 《关于小程序隐私保护指引设置的公告》。大概意思就是对于某些隐私接口,如相册,地理位置啥的,要给用户弹个窗,需要用户点同意以后才能调用。公告内容很多,总结起来有两个重点:



  1. 这个弹窗要你们自己做哟。

  2. 9月15号生效呦,在此之前要是没做弹窗,你的小程序就死定了呦。


然后小伙伴们不敢怠慢,纷纷开始研究怎么个改法,社区论坛里乱成了一锅粥。


有看文档看不懂的:


Screen Shot 2023-09-15 at 12.46.41 AM.png


有没看到公告早上上班突然发现接口都出错一脸懵的:


6a5f760e492e69d02606d9339c33ea8a.jpeg


Screen Shot 2023-09-15 at 12.56.09 AM.png


还有先知先觉的:


Screen Shot 2023-09-15 at 1.05.15 AM.png


骂街的我就不贴了,以防不能过审。


发展


由于推出的过于仓促,阻碍了开发调试,引起开发者不满,官方很快又默默回滚了此次更新。并且鉴于大家都看不懂文档,也不知道怎么改代码。在小伙伴们的强烈呼吁下


Screen Shot 2023-09-15 at 1.17.53 AM.png
官方答应尽快推出demo。


终于在8天之后的8月22日,距离9月15日大限还有24天的时候,大家翘首以盼的官方demo终于出现了。而且不是1个,是3个(后来又多了1个,一共4个)。这下一脸懵变成三(四)脸懵了。


3d769c68d1e39eb2cfc670d8f47af595.jpeg


你让我用哪个???


demo都有了,那就开干吧,还有二十几天,来得及。于是天真的小伙伴们开始高ma高ma兴lie兴lie的写bug。这次改动不仅仅涉及代码层面,还需要后台配置,前台配置,更新缓存,uni/taro框架,基础库版本,第三方开发等等各种问题,大家也是磕磕绊绊的慢慢的都摸清楚了该怎么做。


由于此次改动涉及到所有的小程序,影响面太大,随着9月15号大限的临近,更多的问题渐渐浮出水面。


首先就是那些维护着几十上百个小程序的小朋友,改动看似不多,但每一个都要走开发测试发版流程,而且必须在这短短的不到一个月的时间内完成,听起来就头大有木有?改不完,根本改不完,加班加点不睡觉也改不完。


然后就是那些在线上跑了好几年没更新的小程序,有的是源代码都找不到了,有的原来的开发者早就提桶跑路了,直接欲哭无泪。


还有后知后觉的,剩下几天就到大限的时候来社区里问这个隐私协议是个啥?这么重要的事情平台为什么没有早点通知我???


最后,就是审核变的奇慢无比,原来几个小时就有结果,而现在有小程序审核了好几天了还是没有反应。这其实也是可以预见的,这种影响所有小程序的改动必然会导致版本发布量大增,微信这是人为的自己给自己制造了个审核DDOS,但后果都是开发者承担。就问你急不急吧


Screen Shot 2023-09-15 at 2.20.05 AM.png
就剩几天的时候,有小伙伴被逼的没办法了,不得不使用了一年一次平时打死都不敢用的加急审核。


当时间来到今天,也就是9.15大限前最后一天的时候,上了车的暗自庆幸,没上车的放弃治疗,尘埃即将落定,大家各安天命的时候,意想不到的转折又来了。。。


高潮


9月14号晚上20点38分,也就是距离9月15号0点还有3个多小时的时候,微信官方又发布了一条公告:


Screen Shot 2023-09-15 at 2.49.07 AM.png
总结下来就两点:



  1. 原来说大限是还剩3个小时的9月15号,新的大限推迟到10月17号。

  2. 代码不改也没关系,平台自己会弹窗。


啊???????????????


啥???????????????


就剩3个小时了你给我来个180度大转弯???????????


不瞒你们说,我晚上看到这个公告的开始的表情是:


96282a4cc8aa3522a78448bd660be56a.jpeg
然后是


210235eea23d7fcc4867ae9bbccb3df9.jpeg


这下原本上了车的直接被180度逮虾户给甩出去了,合着我吭哧吭哧费劲巴拉的折腾了一个月眼看就剩3个小时了你告诉我都是白干???你平台为什么不一开始就把方案定成你们自己处理,非要折磨我们开发者???我加班都是白加了?我的用掉的加急审核次数能不能退给我??我怎么觉得有一种被人耍了的感觉?


离最后期限3个小时啊,不是3周,也不是3天,是3个小时啊。你们官方人员业余都是玩赛车的还是兼职开渣土车的啊,平时上下班开车是不是都是漂移过弯啊。


未完待续


这事就这么完了吗? no no no,我觉得还没完,接下来这几天社区里估计又要炸锅了,大家喜欢看热闹的没事可以去围观一下。这不又延期了一个月吗?再出什么幺蛾子我是一点都不会感到意外了。


最后,我觉得这件事印证了我之前听过的一句话,“这个世界就是个巨大的草台班子”。


最后的最后,拿社区里最经典的一张动图镇楼


0-2.gif


作者:ad6623
来源:juejin.cn/post/7278517841884266536
收起阅读 »

记录一下27岁的前端,从二本到澳洲🦘的故事

前言 转眼在悉尼已经206天了,也算是跟大家走了一条不太一样的道路,想还是写下一点东西。为自己作记录,也可以让大家在摸鱼之际看看不太一样的故事。 长文预警,或许有点碎碎念,可以根据目录酌情跳转。 大学的故事 我的大学在成都的一个二本院校读的计算机专业,算不上...
继续阅读 »

前言


转眼在悉尼已经206天了,也算是跟大家走了一条不太一样的道路,想还是写下一点东西。为自己作记录,也可以让大家在摸鱼之际看看不太一样的故事。


FF826FEE-B67F-4661-9728-8139C4132183_1_201_a.jpeg


长文预警,或许有点碎碎念,可以根据目录酌情跳转。


大学的故事


我的大学在成都的一个二本院校读的计算机专业,算不上好,但也没那么坏。




  • 通宵跟室友开过黑,后来上了钻石。




  • 学了网球,当过网球社社长并且一直打到了现在。


    8CEBB138-EC6E-4659-90E9-24CB94747566_1_105_c.jpeg




  • 大二暑假去了美国看过外面的世界,回来结果留了一级🙈。


    899BEF1F-CC21-433A-9872-EF8D0AE2C9CE_1_105_c.jpeg




  • 在学校拿过英语演讲比赛第一名,后来代表学校去参赛见到了好多好优秀的同龄人。


    A5ABE88B-DFB0-498F-820F-8726A3606069_1_102_a.jpeg




  • 去星巴克打过工发生了很多有趣的故事。


    773B0525-473A-4B07-8D43-A652CDBE8E02_1_201_a.jpeg




但要说最大的收获,还是认识了我老婆,陪伴着我一直走到了许多至暗时刻,直至现在:


9548EBF0-5F0A-4803-9046-CB00CEFE30CC_1_105_c.jpeg


毕业以后


创业 Team


毕业之后第一家上班的公司是一个小的创业团队,当时算上老板一共才五个人。


但是依然非常感谢那个机会,我还记得入职的第一天,后端小哥让我看一下接口报错信息,我甚至不知道在 devtools 中如何找到 Networks 😄


后来,我在那里的几个月学会了 JavaScript,学到了简单的 Vue,做了一大堆没有成功的小程序。虽然回过头去看技术不那么酷,但自己凭借自己的三脚猫功夫也算入了门。


5295BBF8-FB29-438B-96B3-73E507A62A08_1_105_c.jpeg


本地房产媒体公司


后来因为要买房子,想获取更多的信息,机缘巧合入职了一家本地的房产媒体公司 🏠。主要做小程序和后台的开发,虽然工作强度不大,但在那里略微拓宽了自己的技术。有机会把学到的 React 上生产环境,后台依然用 Vue 也没有落下,还积累了不少的运维经验,有的用到了现在。


485E4E11-0870-4B8E-A03C-0694E860CCCB_1_105_c.jpeg


下班看到的行色匆匆的人们


医疗信息系统


在工作到第二年的时候,发现自己陷入了重复的怪圈当中。因为业务得限制,技术也不需要很大,所以一直没啥进步,于是决定去一下更能提升自己的公司。


在水群的过程中认识了很好的朋友,在他的推荐下进了一家阿里出来的团队。


8893E40B-94EE-48ED-B7EE-FFF1CE3291E1_1_201_a.jpeg


我在那里度过了飞速成长了一年半时光,虽然有被大家诟病的 996 等,但团队氛围非常好,我也承担了更多的责任,反而没觉得那么累。


在那里的时间,我精进了自己的 React 技术,维护开发过内部的组件库,开发过内部的医疗系统的富文本编辑器,给大家培训过算法。可以说是痛并快乐着的日子。


C7B6D439-E4B6-4D24-82B8-CB1137209638_1_105_c.jpeg


萌生出国的念头


其实到了现在,出国的心情已经跟最开始完全不太一样了。


在一切的最开始,是21年中国互联网开始裁员的那一段时间,阿里腾讯等大手笔地裁员甚至一度让未来科技城的房价下跌。彼时的我还在吭哧吭哧地魔改 antd 的代码,水群的时候偶然看到一条被裁员房贷还不下去还因为高龄找不到工作的消息,突然陷入了对自我的反思。开始觉得如果这种事情发生在我的身上,我会不会有什么不一样。


想了很久,答案是似乎没有什么不同。新闻中的老哥工作的我曾经梦想的“大厂”,拿着令人羡慕的工资,拉满了杠杆买房但却无情地被经济周期抛弃。虽然讨论了这么多年的年龄焦虑,曾经我也是“只要技术硬,不怕年纪大”中的一员,但当站在高龄失业的路口的时候,摆在我面前的似乎只有“降薪”和“牺牲家庭更拼命地走上管理岗”这两条路径。但我也见过自己曾经的领导被卡在高层和一线之间左右为难的境地,楼道间抽完一根烟却也只能苦笑着对团队说“我们这个周末加一加班”。


似乎没有办法改变身边的环境,那么唯一的答案就是只能改变自己。


幸运的是我曾经见识过外面的世界,也明白走出国门并不是那么遥不可及,只要一步一步往外迈,总是能够做到的。



写到这里的时候再次感慨自己何其幸运遇到了一个好的对象,支持我做出的如此巨大的决定,并愿意辞去自己白领的工作跟我走出来。



一个有意思的事情,也在跟一些朋友聊过出国这件事情后发现出其地一致。
当一旦你的心中埋下“出国”这颗种子之后,你会发现你身边所有的信息在一点一点的改变,以前自己从来没有接触过的观点和信息会开始进入你的世界,一次一次击碎你的三观,让你不断的反思自己和周围。


出国的准备


当我们两个确定了自己要一起做这件事情以后,也只是迈出了漫漫长征的第一步。


我们需要选定目的地,需要说服各自的父母,需要做好中短期的规划。


选定目的地


在选目的地之前,我们首先列出了自己的条件,排除掉不可能的路径。例如:我们只有很少的积蓄,决定了我们无法一开始通过读书的方式出国;再者,我的技术水平、经验还有学历还没有强到可以 offshore 直接面试上岸让雇主担保的程度。那么在排除掉了“读书”、“雇主担保”两条路径之后,我们开始了解各个国家。


我在这里列出几个当时我们自己的选项,过多的不再展开:



  • 日本 —— 老婆对它没有什么特殊的情结,不值得放弃一切去日本。

  • 美国 —— 最好,但难度太大,花费太高。

  • 欧洲 —— 备选

  • 加拿大 —— 备选

  • 澳洲 —— 备选

  • 新西兰 —— 备选


在划定完范围以后,就开始做签证的攻略了。


咨询加拿大


最开始我们是有做加拿大的攻略的,了解到加拿大政策稳定,对华人也较友好,除了天寒地冻以外,也挑不上什么毛病。于是我们认真的了解了政策,每天中午午休的时候就省去了睡觉的时间了解每一个省的政策,中间还认真花费咨询了持牌律师,然后选定了曼省配偶工签再走省提名的道路。


方法方式敲定了以后,我们又陷入了纠结的境地。当时算下来老婆读书要20W的学费,加上租房生活,还有签证律师费等一系列的费用,要准备4-50W。算出来数字的那一瞬间,我们知道还是跟我们无缘了。


了解 WHV


老婆突然有一天想起 WHV 这个签证类别,跟我说2022年的名额开放了。虽然我们早就知道这个签证类型,但因为它本质上还是给背包客们一个旅行干劳力活儿的临时签证,当时没有想到跟我们任何关系。但我后来认真的研读了它的签证条款后,发现它并不局限于签证持有人的劳动行业,只是不能为同一个雇主工作超过6个月,于是一条似乎可行的路径在我心中萌发出来。



裸辞,拿着WHV入境,落地后在境内找工作,工作一段时间卷出一定的成果后让雇主给我做担保。



并且这个签证的成本非常之低,只需要出450AUD的签证费,考出雅思4.5分或者PTE Academic 30分即可。


2022年澳洲政府将这个签证由之前的先到先得改成提交EOI等待邀请的形式,甚至提交EOI表格的时候不需要任何材料辅助,完全可以受邀再去考英语。


那就没有什么好商量的,我们挑了一个风和日丽的上午,将两个人的EOI都递进了澳洲官网,从此命运的齿轮发生了转动。


与此同时我们同时还花钱准备下来了新西兰的 WHV 签证,但这一部分按下不表。


前后受邀


递交完EOI的日子里,我们的生活在有条不紊地进行。我换到了一家五百强的外企继续做前端,在成都完成了我们的婚礼,也挑了好的时候跟双方父母表明了这个事情。更加幸运的是,我们双方的父母都非常开明支持我们的决定。我们看到过WHV交流群中有的小伙伴父母听到“出国”两个字,甚至以死、断绝关系来要挟孩子不让离开。我们倍感幸运,同时也感慨世界之大。


先受到澳洲政府邀请的是我老婆,但我们没有特别大的波动,如果我最后没拿到的话,依旧是没有意义的事情。


好在,我大概在一个月之后就在邮箱中收到了邀请递签的信。


image.png


于是,身份有了,接下来就是准备出发的事情。


正式出发


我们在双双受到邀请后的一个月递交了自己的签证申请,在等待下签的同时我们也没闲着。


老婆去跟她实习到毕业到最后一天的公司说了再见,整整三年,即是解脱也充满了忧愁。为了让自己有一技之长,她选择辞职以后去报班学了美甲💅,也为我们的生活增添一分可能性。


我也去跟 Team Leader 表明了想法,离开了在很多人眼里在成都非常好的工作。


7F01C619-D970-4F80-BF07-90681031CDE8_1_105_c.jpeg


image.png


我们决定了在过完年后出发,于是开始处理自己在成都几年积累下来东西,把自己结婚买的新车卖掉了;和住了很久的房子说再见,和很多好朋友告别,在2023年元旦过后回到了老家,然后定了2月21号出发的机票。


0A77260A-42FE-492F-95BF-8DB6005FCE2D_1_105_c.jpeg


image.png


在家里无忧无虑的日子还是过的飞快,转眼就到了要出发的日子。那个时候出境还需要48小时核酸,我们因为害怕感染,愣是三天都没有出过家门。


4DE70FCA-3201-498A-BA8E-1F294193216C_1_105_c.jpeg


从重庆 -> 厦门 -> 悉尼


B4B0A5BA-55B0-4A97-A5A3-D3CD0FD661D4_1_105_c.jpeg


2A130C69-71EF-4314-95E3-7C457E9B4476_1_105_c.jpeg


找工作的那些日子


落地之后我们选择住了三天酒店,因为租的房子的起租期在三天之后,然后又因为悉尼的正好在举办 Sydney WorldPride 🌈 节,世界各地的人都涌过来参加活动,酒店超级贵。我们租了一个角落里的房间三天,那是我们俩此生住过最小的房间,2000RMB。


595431C2-A897-49B3-A13A-C819973C1B40_1_105_c.jpeg


A00FEDB6-5855-4201-B0A2-4114EE82DC49_1_105_c.jpeg


落地后发现自己的英文完全属于没法用的阶段,便利店买东西也听不懂,办银行卡也听不懂,但磕磕绊绊两个人也还是一起完成了下来。不敢懈怠,第二天就开始把自己的简历电话号码更新成了当地号码,LinkinIn 状态改成了 open。期待着能够开始有一些面试或者电话,因为每天花澳币几乎是五倍的生活开销,心理压力实在是太大。


3C01548D-63DB-4F6C-BB91-69EBD3347588_1_105_c.jpeg


还记得躺在酒店的第二天晚上,我刷了一晚上小红书,因为发现了送外卖能赚到一些钱而非常激动。第二天早上很兴奋地跟老婆说,“我要去送外卖啦!”


第三天将我们巨多的行李箱搬到了租的一室房子内,房租 1200AUD 每周:


7521FA19-5105-443E-BED8-CFF7E04F1A61_1_105_c.jpeg


折腾了一天之后,看着窗外的夕阳🌇,发了一条朋友圈:


image.png


我戏谑地跟老婆说,我们毕业的第一年住的一室的房子,后来逐渐换得像一个家。现在又回到了一室的房子中,这次不知道再要住多久了。


7E1E2567-7B36-49E5-BEB4-DBA04420D3B6_1_105_c.jpeg


因为澳洲租的房子大多完全不带家具,我们头一个星期完全处于淘家具的过程中,家中不断地受到网上买的便宜家具包裹,然后安装:


4F40CE21-6E89-4356-8B4C-3B764C54F5B3_1_105_c.jpeg


在睡了一个星期地板的,人生第一次自己安装了床架:


277B3E14-1661-4885-A390-18E7FC704A02_1_105_c.jpeg


第一次安装了柜子:


9B2E8FCF-34F0-4EDB-8E5B-995B4A871E94_1_105_c.jpeg


家徒四壁依旧坚持面试:


F75880C1-D99E-456B-A182-88BF70BEF690_1_105_c.jpeg


中途去看了周董疫情后的第一场演唱会:


A1FADDD8-210F-4633-BB85-64E8B7E2F3CA_1_105_c.jpeg


给老婆圆了她一直的梦,用我们卖车的钱买了 Mini Cooper:


F4C5C70D-3C0C-4E2C-A071-3EECF7B3F242_1_105_c.jpeg


买了车之后,就一直在送外卖:


0923ED08-ADAB-49A3-A3F2-A4CA00663E97_1_105_c.jpeg


还记得送外卖的第一天,我们中午从11点跑到了2点,回来休息了2个小时,从4点跑到了8点,赚了100多刀。我们兴奋地去超市里买了很多的水果:


DAB0A1CE-C43F-499D-A99F-714832D3CFEF_1_105_c.jpeg


因为这边东西太贵,并且家里没有冰箱。我们吃了整整一周的速食食品,直到送外卖开始赚生活费:


D2BAFBB5-7BC2-4E4A-AD61-9517C937C097_1_105_c.jpeg


AF07C3C3-B406-4425-8A07-EB1663CEFC5F_1_105_c.jpeg


中途送外卖一度开心到忘记了自己曾经是一个程序员,笑。


9624BF24-2C40-4961-9D7F-3D7B60138020_1_105_c.jpeg


后来很多简历投递到公司邮箱,除了拒信就是没有回复,我变得十分焦虑。跟家里人打电话的时候也在说,只能再给自己两个月的时间了,家里人支持一个月,自己的生活费能再撑一个月,5月还没有找到工作就要回家了 🙈


于是我开始反思自己的方式方法。痛定思痛之后决定主动出击,厚着脸皮开始在 LinkedIn 上私信猎头和工程师,寻找内推的机会。


换了新的方法之后,果然成功率要高了不少,尽管有一半以上的人不会回复我的消息,但剩下的一半也至少让我开始收获一些电话询问我的情况。


image.png


然后陆陆续续地开始接到一些面试,但刚开始大多都还是 recruiter 和猎头了解情况的面试。我深知自己没有任何本地的工作经验,口语也算不得好,因为珍惜每一次能跟别人打电话的机会,尽量表现出热情,结束后也会记录自己的表现:


image.png


在足够多的厚着脸皮尬聊之后,终于迎来了生活的曙光。猎头给我打电话有澳洲最大的保险公司之一在招聘一批 contractor,并迅速地帮我安排了第二天的面试。也许这次运气比较好吧,顺利的通过了面试,拿到了6个月的合同,生活可以稳定一段时间了。


还记得在受到 offer 的那天晚上,我回家的路上,想起了在再次收到拒信的那个晚上看《当幸福来敲门》,一瞬间理解了 Chris 收到了正式 offer从办公室走出来的心情:


image.png


工作的时光


入职的一点小震撼


入职的第一天是我和一个父母中东但在这边出生的小哥一起办理入职,我的 Manager 带我们领了电脑,中午一起吃了饭。但是英语闲聊也太太太难了,我现在都记得第一天完全不知道他们在说什么...


但没过多久澳洲职场就给我这个中国小伙子来了一个大大的震撼:当天下午四点,我还在工位上吭哧吭哧 yarn install。中东小伙过来拍了拍我的肩膀,“你要去火车站吗?要不要一起?”,我看了看表,又看了看他真诚的目光,点了点头。于是我们去跟 Manager 说我们走了, Manager 面不改色,问我们的 VPN 的调通了吗,确保可以在非办公室网络下连上了吗?我说弄好了。她点点头说,那就好,这样你明天就可以在家工作了。


我当时听到并没有当一回事,只当是新人入职的客套。于是平生在4点30分搭上了回家的列车。


image.png


第二天,我高兴地背上电脑和小书包来到 CBD 的办公室,结果空无一人。早会的时候,我说我在办公室,结果大家一脸震惊,说你昨天不是去了吗?为什么今天又去?于是我在第三天顺理成章地居家办公了,直到今天,我还大概保持着一个月去一天的通勤频率。


image.png


技术栈


其实技术方面反而没什么好聊的,跟国内比起来,这边对技术的要求反而没这么高,对候选人的资质也更加宽容。


我们用的是 React + NestJS + AWS 的一揽子全栈方案,没有什么学习成本也就上手了。


在🦘工作和国内的区别


Make sure you are happy and health


在写下这一篇小记的时候,正是澳大利亚的 R U OK Day,每一个 manager 都会买上一些小吃或者礼物,关心自己的员工是否高兴和健康。


例如我们 Manager 最常用来总结早会的一句话就是:“如果你有感觉到任何不舒服或者对项目不那么满意的地方,请不要犹豫地找我聊聊,我的存在就是为了让团队更好的运转。”


9F0DBC26-3B0B-4869-8A02-8316A7CA2C3F.jpeg


大家在早会开始之前,也会毫不忌讳地在群里发出“I'm feeling unwell today. Will taking a day off”,然后潇洒地消失一整天。


3AA7D1D8-68CD-4D32-BCE2-1A709A20B5AB.jpeg


工作与生活的界限


在这边,天大的事情,也几乎不会在你下班之后给你发任何消息。我在下午5点之后收到过几次其他同事或者 Manger 的消息,开头的第一句话必定是道歉。至于晚上和周末,则一次都没有。


235A7542-085B-4817-855D-1A1AD595E70C_1_201_a.jpeg


对职业的容忍度



  1. 我们团队有两个工程师甚至没有上过大学,高中毕业自学编程。他们很大方地在闲聊中承认,团队中的其他人也没有觉得有任何问题。

  2. 有39岁转码带娃的妈妈,她很努力,我们都很喜欢她。

  3. 我们团队的 Senior Engineer 以前是仓库管理员。


后记


澳洲的工作很理想,但这边的生活却和国内大相径庭。在略微稳定下来以后,我们俩又经常思念国内有家人朋友在身边的日子,思念出门就可以吃到美食的日子,思念我们熟悉的文化和乡土。


窗外的明月高挂,不知道下一次回家又是什么时候了。


以此为记。


作者:yuetong3yu
来源:juejin.cn/post/7278929122302132279
收起阅读 »

又一个全新编程语言,诞生了!

最近,编程领域又一个黑马忽然冲进了开发者们的视野并正式开放下载。 它的名字叫Mojo,相信有不少小伙伴最近也看到了。 Mojo是为AI开发者所准备的编程语言,语法有点像Python。 根据Mojo官网的描述,它结合了Python的易用性和C语言的高性能,解...
继续阅读 »

最近,编程领域又一个黑马忽然冲进了开发者们的视野并正式开放下载。


它的名字叫Mojo,相信有不少小伙伴最近也看到了。



Mojo是为AI开发者所准备的编程语言,语法有点像Python。



根据Mojo官网的描述,它结合了Python的易用性和C语言的高性能,解锁了AI硬件的可编程性和AI模型的可扩展性。


Mojo看起来好像挺能打,它到底是哪个公司所推出来的呢?


看了一下才发现Mojo是由人工智能公司Modular所推出的全新编程语言。


而Modular这个公司则是一个非常年轻的新生AI创业公司,于2022年由Chris Lattner和Tim Davis所创立。



提到这两个创始人,相信有些同学也有所了解,都是业内顶级专家。其中Chris Lattner还被称为“LLVM之父”和“Swift之父”,在苹果、谷歌、特斯拉等多家知名科技巨头里曾带领构建了AI和核心系统。


Modular公司的愿景非常宏伟,目标是自下而上重塑AI基础设施。


去年的时候,Modular AI曾获得过3000万美金的融资。而就在前些天,Modular又再次宣布成功融资 1 亿美金,这对于一个刚诞生不久的初创型公司而言可谓是成绩斐然。



另外在公司官网的投资者名单里能看到,不少AI领域的知名投资机构都有参与。



Mojo这个编程语言有几个比较明显的特点。


1、首先是性能方面。


Mojo充分利用硬件的特性和功能,包括多核、矢量单元和加速器单元,以及先进的编译器和异构运行时机制,在不增加复杂性的前提下实现了与C++和CUDA相当的性能。


在并行化这一块,Mojo利用MLIR,使Mojo开发者能够充分利用向量、线程和AI硬件单元。



2、其次是互操作性方面。


大家都知道,发展到今天,Python的生态极其繁荣,各种函数、库、框架、模型、工具等等数不胜数。


而Mojo则可以访问整个Python生态。比如使用Mojo,可以在代码中无缝地接入和混合像Numpy和Matplotlib等库。



3、再者就是可扩展性方面。


可扩展性这块也是Mojo的优势。Mojo可以升级用户模型中的已有操作,以便开发者可以使用预处理、后处理、自定义替换等操作来轻松地扩展用户的模型。


Mojo最初发布于今年的5月初,上线数月以来就已形成基本规模和生态。



前不久,Modular官网宣布Mojo正式开放下载,首先是从Linux系统开始,并在后续的迭代版本中将陆续添加对Mac和Windows的支持。


这也意味着开发者可以通过Mojo SDK进行尝试并编写自己的Mojo代码。



而就在Mojo官宣可以下载后不久,一位名叫Aydyn Tairov开源作者就利用Mojo来做了一个突破性的尝试。


这个作者之前曾将GitHub上火热的由纯C实现的llama2.c项目移植到了基于Python的llama2.py。


而这次Aydyn Tairov又将llama2.py移植到了llama2.mojo,结果非常出乎意料,移植后性能提升了近250倍。



即便如此,作者仍然认为里面还有一些改进的空间。


看到Mojo如此的表现,有不少网友说Python这次可谓是遭遇了一个强大的对手,Mojo甚至有可能在未来会取代Python?


对此,公司CEO Chris Lattner直接回应称:


Mojo并不会对Python造成威胁,相反,还会帮助Python开发者变得更强大。要担心的也不是Python,而是C++们。



文章的最后也附上相关的页面,感兴趣的小伙伴可以尝试一下。



至于这门编程语言在接下来的AI时代会发展如何,我们可以拭目以待。


作者:CodeSheep
来源:juejin.cn/post/7280057907055902760
收起阅读 »

环信web、uniapp、微信小程序sdk报错详解---登录篇

项目场景:记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40 (一)登录用户报400原因分析:从console控制台输出...
继续阅读 »

项目场景:
记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40

 (一)
登录用户报400



原因分析:
从console控制台输出及network请求返回入手分析
可以看到报错描述invalid password,密码无效,这个时候就需要去排查一下该用户密码填写是否正确


排查思路:
因为环信不保存用户的密码,可以在console后台或者调用修改密码的restapi来修改一下密码再重新登录(修改密码目前只有这两种方式)



(二)
登录用户报404




原因分析:
从console控制台输出及network请求返回入手分析
可以看到报错描述user not found,这个时候就需要去排查一下该用户是否存在于该项目使用的appkey下了

排查思路:
可以看一下console后台拥有这个用户的appkey和自己项目初始化时用的是否是同一个,若在console后台并没有查到该用户,就要注意这个用户是否真的没有注册




(三)
登录用户报40、401




原因分析:
报错40或者401一般都是token的问题,需要排查一下token是否还在有效期,token是否是当前用户的用户token
40的报错还有一种情况,用户名密码登录需要排查用户名及密码传参是否都是string类型


注:此处需要注意用户token和apptoken两种概念
用户token指的是该用户的token,一般只用于该用户在客户端使用环信 token 登录和鉴权
app token指的是管理员权限 token,发送 HTTP 请求时需要携带 app token
token较为私密,一般不要暴露出去

排查思路:
排查用户名及密码传参是否都是string类型,这个可以直接将option传参打印出来取一下数据类型看看是否是string
关于token排查,现在没有合适的办法直接查询token是否还在有效期或者是不是当前用户的token,只能通过api调用看是否报错401,可以在console后台直接获取新的用户token来测试一下


是不是当前用户的token也可以找环信的技术支持帮忙查,但在不在有效期他们也查不了


话外
有人遇到为什么已经open成功了但是还会报错

这里要注意open只能证明获取到了token,证明不了已经建立了websocket连接,只有触发onOpened或者onConnected回调 只有onOpened或者onConnected回调触发,才算真正与环信建立连接。所以也不能在open返回的success或者.then中做任何逻辑处理,此外还要注意监听回调一定要放在调用api之前,在调用任何一个api时都要保证监听挂载完毕,包括open


如何判断自己是否在登录状态

可以用以下三种方法中的一种判断当前用户是否在登录状态~
1、WebIM.conn方法下有一个logOut字段,该字段为true时表明未登录状态,该字段为false时表明登录;
2、WebIM.conn.isOpened () 方法有三个状态,undefined为未登录状态,true为已登录状态,false为未登录状态,可以根据这三个状态去判断是否登录;
3、通过onOpened 这个回调来判断,只要执行了就说明登录成功了,输出的话,输出的是undefined
三者选其一判断登录状态

收起阅读 »

需要具备哪些技能才算中高级前端?

之前有人问过我,“到底什么样才算中高级前端,需要具备哪些技能才算中高级?”他的本意是让我推荐一下前端的学习路线,然后再问了我这个问题,估计是想看看有哪些技术是晋升中高级前端的关键,提前学习吧。 这里不管是前端、终端还是后台,我觉得是可以统一来讨论的。 有什么标...
继续阅读 »

之前有人问过我,“到底什么样才算中高级前端,需要具备哪些技能才算中高级?”他的本意是让我推荐一下前端的学习路线,然后再问了我这个问题,估计是想看看有哪些技术是晋升中高级前端的关键,提前学习吧。


这里不管是前端、终端还是后台,我觉得是可以统一来讨论的。


有什么标志性的技能或者技术是可以作为中级工程师和高级工程师的分水岭的吗?只要学会了这些技术和技能,就一定可以晋升中高级工程师?我想是没有的。


我分享一下我对初中高级工程师的理解,仅供参考。


初级工程师就是应届毕业生,标志是能够熟练支撑中小型业务需求开发。他可能会支撑所有业务模块的开发,或者非核心业务模块的开发,同时也会支撑基础技术项目的开发。所以,如果使用是否参与基础技术项目来作为判断的话,是不对的。


中级工程师的标志是能够独立负责一个核心模块。成为一个模块负责人,这个模块的所有事情,领导都可以放心交给你的时候,你就是中级工程师了。这个负责模块,不是指能够支撑涉及这个模块相关的需求。而是指,你要:


  • 了解它的全部代码、它的设计原理
  • 了解它在整个系统中的位置、它跟其他模块的关联关系
  • 了解它的各种特性、现状、问题、未来的优化、发展方向
  • 维护好它的文档
  • 可以很好地给其他人、你的领导描述清楚,这个模块的所有内容
  • 负责它的一切

高级工程师的标志是能够负责一个系统成为系统负责人,带领项目成员一起,承担这个系统的所有事情。对比中级工程师,负责的内容更大更加复杂了,但本质没变,就是要综合能力。同时,中级工程师还只是单人作战,如果想要成为高级工程师,一定需要了解团队的力量,并学习如何通过合理的项目管理手段,做好一个复杂系统。


这里中级和高级都提到了“负责”这个词,那具体怎样才算负责,是领导指派给你,让你负责一个核心模块,就算负责了吗?不是的。这里的“负责”是指能够完全胜任,做出让领导满意的成果,让领导非常放心


当然,每家公司对不同职级的能力要求是不一样的,你也可以完全按照上面的能力描述来进行有针对性的学习和成长。


以上就是我对于中高级前端开发的理解,希望能够给你带来一些启发。



【讨论问题】


你是如何理解中高级工程师的呢?


欢迎在评论区分享你的想法,一起讨论。



----------------【END】----------------



【往期文章】


给你介绍一个工具,帮你找到未来的努力方向


《程序员职场工具库》高效工作的神器 —— checklist


2023 年上半年最值得看的一篇文章



欢迎加我v【longyiyiyu】,进行无负担沟通,我会


  • 长期职业发展规划指导
  • 近期工作重点交流
  • 职场解惑
  • 面试辅导

也欢迎关注公众号【潜龙在渊灬】,收获程序员职场相关经验、提升工作效率和职场效能、结交更多人脉。


作者:潜龙在渊灬
链接:https://juejin.cn/post/7274902683404206143
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我又听到有人说:主要原因是人不行

在工作中,我们经常把很多问题的原因都归结为三个字:人不行。 曾经有UI同事指着我的鼻子说,你们没有把设计稿百分百还原,是因为你们人不行。 昨天,我又听到一个研发经理朋友说,唉呀,项目干不好,主要是人不行。 哦,我听到这里,有种似曾相识的感觉,于是我详细问了一...
继续阅读 »

在工作中,我们经常把很多问题的原因都归结为三个字:人不行。



曾经有UI同事指着我的鼻子说,你们没有把设计稿百分百还原,是因为你们人不行


昨天,我又听到一个研发经理朋友说,唉呀,项目干不好,主要是人不行


哦,我听到这里,有种似曾相识的感觉,于是我详细问了一下,你的人哪个地方不行。



朋友说,项目上线那天晚上,他们居然不主动留下来值班,下班就走了,自觉意识太差。代码写的很乱,不自测就发到生产环境,一点行业规范都没有。他们……还……反正就是,能不干就不干,能偷懒就偷懒,人不行!



这个朋友,代码写的很好,人品也很好,刚刚当上管理岗,我也没有劝他,因为我知道,劝他没用,反而会激怒他。


当一个人,代码写得好,人品好,他就会以为别人也和他一样。他的管理方式就会是:大家一定要像我这样自觉,不自觉我就生闷气了!


反而,当一个人代码写得差,自觉性不那么强,如果凑巧还有点自知之明,那么因为他很清楚自己是如何糊弄的,因此他才会考虑如何通过管理的方法去促成目标。


我的这些认知,满是血泪史。因为我就经历过了“好人”变“差人”的过程。


因为代码写得好,几乎在每一个公司,干上一段时间,领导都会让我做管理,这在IT行业,叫:码而优则仕


做管理以后,我就发现,并不是所有人都像我一样,也并不是各个部门都各司其职,所谓课程上学的项目流程,只存在于理想状态下。当然,其中原因非常复杂,并不一定就是人不行,也可能是流程制度有问题。比如我上面的朋友,他就没有安排上线必须留人,留什么人,留到几点,什么时候开始,什么标准算是上线完成,完成之后有什么小奖励,这些他都没有强调和干预。


但是,我们无法活在理想中。不能说产品经理的原型逻辑性差,UI的设计稿歪七扭八,我们就建议老板把公司解散吧,你这个公司不适合做软件产品,那样我们就失业了。


你只能是就目前遇到的问题,结合目前手头的仅有的仨瓜俩枣,想办法去解决。可能有些方案不符合常规的思路,但都是解决实际问题特意设置的。


比如我在项目实践中,经常遇到的一点:



产品经理没有把原型梳理明白,就拿出来给开发人员看,导致浪费大家的时间,同时也打击大家的积极性:这样就开始了,这项目能好的了吗?我们也做不完就交给测试!



这种情况,一般我都会提前和产品经理沟通,我先预审,我这关过了,再交给开发看,起码保证不会离大谱。这里面有一个点,产品没有干好自己的活,人不行?他也只有3天时间设计原型。


还有一个问题也经常出现:



即便是产品原型还算可以,评审也过了。让开发人员看原型,他们没有看的。一直到开发了,自己的模块发现了问题,然后开始吐槽产品经理设计的太烂,流程走不通。



这是开发人不行?他们不仔细看,光想着糊弄。其实是他们没有看的重点,你让我看啥,我就是一个小前端,让我看整个平台吗?让我看整个技术架构?Java该用什么技术栈?看前端,你告诉我前端我做哪一模块的功能?此时,我一般都是先分配任务,然后再进行原型评审。如果先把任务分下去,他知道要做这一块,因为涉及自己的利益,会考虑自己好不好实现,就会认真审视原型,多发现问题。这样会避免做的过程中,再返过头来,说产品经理没设计好。已经进入开发了,再回头说产品问题,其实是开发人员不负责,更确切说是开发领导的责任。


一旦听到“人不行”的时候,我就会想到一位老领导。


他在我心中的是神一般的存在,在我看来,他有着化腐朽为神奇的力量。


有一次,我们给市场人员做了一个开通业务的APP:上面是表单输入,下面是俩按钮,左边是立即开通,右边是暂时保存。后来,市场同事经常找我们:能不能把我已开通的业务,改为暂时保存,我点错了。这点小事还闹到公司大会上讨论,众人把原因归为市场推广的同事人不行:没有上过学?不认识字?开不开通自己分不清吗?


此事持续了很久,闹得不愉快。甚至市场部和研发部出现了对立的局面,市场部说研发部不支持销售,研发部说市场部销售不利乱甩锅。


我老领导知道后,他就去了解,不可能啊,成年人了,按钮老按错,肯定有问题。原来,客户即便是有合作意向,也很少有立即开通的,他们都会调查一下这个公司的背景,然后再联系市场人员开通。两个按钮虽然是左右平分,但是距离很近。于是,他把软件改了,立即开通按钮挪到上边,填完信息后,顺势点击暂时保存,想开通得滑到上面才能点击。此后,出错的人就少了。




后来,行政部又有人抱怨员工人不行。发给员工的表格填的乱七八糟,根本不认真。有一项叫:请确认是否没有错误_____。明明没有错误,但是很多人都填了“否”。尽管反复强调,一天说三遍,依然有人填错,没有基本的职场素质。


老领导,他又去了解。他把表格改了,“是否没有错误”改为“全对”,空格改为打钩。后来,填错的现象明显少了。




很多事情,我们都想以说教来控制形势。比如反复强调,多次要求,我嗓子都喊哑了。因为不管是区分按钮,还是填写表格,你不是个傻子,你的能力是可以做到的,不应该出错,出了错你就是人不行。而老领导总是以人性来控制,知道你是懒散的,肯定不愿意认真付出,因此设置一个流水线,让你随着预设的轨迹被迫走一圈。下线后,居然发现自己合格了,甚至自己都变成人才了。用他的话说就是:流程弥补能力不足。



当归因为人不行时,其实分两种情况:别人不行自己不行


作者:TF男孩
链接:https://juejin.cn/post/7146055238741393415
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

离职交接,心态要好

话说今年经历了几次项目交接?主动和被动的都算! 01 实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目; 不断拉扯和管理内心情绪,避免原地裂开; 年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程; 毫无征兆的变动必然...
继续阅读 »

话说今年经历了几次项目交接?主动和被动的都算!




01



实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目;


不断拉扯和管理内心情绪,避免原地裂开;


年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程;


毫无征兆的变动必然会引起一系列问题,最直接的就是影响团队现有节奏进度,需要重新调整和规划;


人员的小规模变动,对部门甚至公司产生的影响是显而易见的,道理都懂;


但是从理性上思考,这个问题并非是无解的,是可以在各个团队中,进行内部消化的;


而人力减少带来的成本降低,以及确保公司的可持续,这是极具确定性的,也是核心目的;


所以感性上说,这个梦幻的职场,可能真的是"爱了";



02



如果是常规情况下的离职流程,交接并不是一件复杂的事情,因为有时间有心情来处理这事,好聚好散;


然而最骚的是,奇袭一般的裁员手段,几分钟谈话结束直接走人;


丝毫不顾及由此带来的影响,认定留下的人应该兜底相应的责任,实现无缝接坑;


当然并不是什么公司都有底气这么做的,大部分还是在裁员通知后,留有一定的时间处理交接事项;


对于交的过程是否有质量,完全看接的一方是否聪明;


从感性上分析,都已经被裁了自然要牢牢把握摸鱼的机会,根本不会在意交出的事项谁来维护,不反越防线就不错了;


而压力会直接传送后闪现到接的人正上方;



03



面对被动离职的交接,确实很难妥善处理,情绪化容易导致事情变质,能真正理性对待的并不多;


交接涉及到三方的核心利益:公司、交出人、接手人,不同角度对待这件事件,态度完全不同;


公司,并不关心交接的质量,只要项目有人兜底即可;


交出方,感性上说直接敷衍交接单上的流程即可,并不在意后续的影响;


接手方,项目交接完成后的第一责任人,可能会关心项目的质量状况;


至于说接手的人能否有时间,有能力,有心情接下这种天降大任,可能除了自己以外,不到出问题的时候关注的很少;


因为项目交接过程没有处理好,从而导致后续的事故与甩锅,情绪化的现象并不少见;


如果是在内部矛盾突出的团队中,由此引发的离职效应也并不少见;



04



人的情绪真的是很奇怪,能让复杂的事情变的简单,也能让简单的事情变的离谱;


情绪上头的时候,事情本身是否真的复杂就已经不太重要了;


接手方最大的问题在于吃力不讨好,如果接了一个质量奇差的项目,意味之后很长一段时间内,工作状态都会陷入混乱的节奏中;


对于大部分研发团队来说,都是存在排期规划的,如果被交接的项目横插一脚,重新调规划影响面又偏大;


向上反馈,多半是回答一句:自行消化;


何谓自行消化,就是占用空闲时间处理,比如下班后,比如周末,比如摸鱼,这些都是对工作情绪的持续伤害;


最终兜底的个人或者团队,可能需要带着夜宵去公司搬砖;



05



吐槽归吐槽,裂开归裂开,成熟的搬砖人不该表现出明显的情绪化;


先捋一捋在面对离职交接时的注意事项,虽然说离职后有一个过渡期,但是真正涉及交接的时间通常一周左右;


作为接手一方,自然期待的是各种文档齐全,对于坑坑洼洼的描述足够清楚;


然而对于被离职的交出方,会带着若隐若现的情绪化状态,很难用心处理交接事项,能不挖坑就已经是良心队友了;


接手方作为后续的兜底人员,兜不住就是一地鸡毛;


如果兜住了呢?那是职责所在、理所应当、不要多想、安心搬砖;



06



面对项目交接,这种隔三差五个月就会突发的事,完全可以用一套固定的模式和节奏去执行;


强烈建议:不排斥、不积极、不情绪化;


但是在处理的过程中要理性且严谨,这样可以规避掉许多可能出现的麻烦,毕竟签了交接单,从此该项目问题根本甩不开;


职场几年,在多次"交"与"接"的角色转换过程中,总结以下几点是研发需要注意的;


P1:文档,信息的核心载体;


不管项目涉及多少文档,照单全收;


如果文档严重缺失甚至没有,直接在交接单上写明情况,并且得加粗划重点展示;


文档和项目的维护极有可能是线性不相关,但是手有文档心里不慌,因为方便后续再把项目交接给其他人;


所以,敷衍一时爽,出事火葬场;



07



P2:代码工程,坑与不坑全看此间;


接到手里的项目,是否会导致情绪崩塌,全看项目代码工程的质量,遇上一堆烂摊子,心情会持续的跌跌跌,然后裂开;


直接把人打包送走的情况也并不少见;


如果代码工程质量极高,架构设计稳定,组件集成比较常规,分包井然有序,悬着的情绪可以适当下落;


P3:库表设计,就怕没注释;


对于数据库层面的设计,与代码工程和业务文档三者相辅相成,把握其中的主线逻辑即可;


但前提是表的设计得有清晰的注释,如果是纯中式英文混搭拼音,且缺乏注释,必然会成为解决问题的最佳卡点;


P4:核心接口,应当关注细节;


从项目的核心业务中选出2-3个复杂的接口读一读;需要将注意点放在细节逻辑上,给内心积蓄一丢丢解决问题的底气;


熟悉接口的基本思路:请求从客户端发出,业务服务的处理逻辑,对数据层面的影响,最终响应的主体;



08



P5:遗留问题,考验职场关系的时候到了;


公司一片祥和的时候,员工之间还可以做做样子;


但是已经走到了一别两宽的地步,从感性上来说只要不藏着掖着就行,还想窥探别人安稳摸鱼的秘密,确实想的不错;


老练的开发常干的事,为了解决某个问题临时上线一段代码,处理好后关闭触发的入口,但是会保留代码主体;


这还算常规操作,最骚的是在本地写一段脚本工具解决线上的问题;


这些隐藏的接口和脚本只有开发的人自己清楚,如果不给个说明文档,这不单是挖坑,还顺手倒了一定比例的水进行混合;


P6:结尾事项,寒暄几句还是要的;


安全意识好的公司,会对员工的账号权限做好备份,以便离职时快速处理,不会留下风险隐患;


在所有权限关闭之后,接手人就可以在交接单上完成签字仪式;


交接完成后还是得适当的寒暄几句,万一接了个坑,转头就得再联系也不稀奇,所以职场留一线方便语音再连线;



09



年度收到的离职交接,已经累计好几份,对这种事情彻底麻了;


事来了先兜着,等兜不住的时候自然会有解决办法;


抗拒与烦躁都不会影响流程的持续推进,这种心态需要自己用清醒的意识不断的说服自己;


最后想探讨一个话题,跟项目前负责人联系,用什么话术请教问题,才能显得不卑不亢?



END


作者:知了一笑
链接:https://juejin.cn/post/7157651258046677029
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Swift - 闭包

iOS
定义 闭包是一个自包含的函数代码块,可以在代码中被传递和引用。闭包可以捕获和存储其所在上下文中任意常量和变量的引用**。 闭包的语法有三种形式:全局函数、嵌套函数和闭包表达式。 全局函数是一个有名字但不会捕获任何值的闭包潜逃函数是一个有名字并可以捕获其封闭函数...
继续阅读 »

定义


  • 闭包是一个自包含的函数代码块,可以在代码中被传递和引用
  • 闭包可以捕获和存储其所在上下文中任意常量和变量的引用**。

闭包的语法有三种形式:全局函数、嵌套函数和闭包表达式。


  • 全局函数是一个有名字但不会捕获任何值的闭包
  • 潜逃函数是一个有名字并可以捕获其封闭函数域内值的闭包
  • 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包

闭包表达式


闭包表达式的一般形式

{ (parameters) -> return type in

statements

}

以数组的sorted(by:)方法为例

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})


写成一行

names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2})

根据上下文推断类型


  • sorted(by:)方法被一个字符串数组调用,Swift 可以推断其参数和返回值的类型,因此其参数必须是 (String, String) -> Bool
  • 这意味着(String, String) 和 Bool 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:
names.sorted(by: { s1, s2 in return s1 > s2})

单表达式闭包的隐式返回


  • 单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果
names.sorted(by: { s1, s2 in s1 > s2})

参数名称缩写


  • Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0$1$2 来顺序调用闭包的参数,以此类推。
  • 闭包接受的参数的数量取决于所使用的缩写参数的最大编号。
  • in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
names.sorted(by: {s1 > s2})

运算符方法


  • Swift 的 String 类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。而这正好与 sorted(by:) 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现:
names.sorted(by: >)

尾随闭包


尾随闭包是一种特殊的闭包语法,它可以在函数调用的括号外部以简洁的方式提供闭包作为函数的最后一个参数。
使用尾随闭包的优势在于增加了代码的可读性和简洁性。当闭包作为函数的最后一个参数时,将闭包放在括号外部,可以使函数调用更加清晰,更接近于自然语言的阅读顺序。

func calculate(a: Int, b: Int, closure: (Int, Int) -> Int) {
let result = closure(a, b)
print(result)
}

// 调用函数时使用尾随闭包
calculate(a: 5, b: 3) { (x, y) -> Int in
return x + y
}

// 如果闭包只包含一个表达式,可以省略 return 关键字
calculate(a: 5, b: 3) { (x, y) in
x + y
}

// 省略参数的类型和括号
calculate(a: 5, b: 3) { x, y in
x + y
}

// 使用 $0, $1 等缩写形式代替参数名
calculate(a: 5, b: 3) {
$0 + $1
}


如果一个函数接受多个闭包,需要省略第一个尾随闭包的参数标签,并为其余尾随闭包添加标签。



值捕获


闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。



可以捕获值的闭包最简单的形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。



注意:



如果将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员捕获了该实例,将会造成一个循环引用。



捕获列表


默认情况下,闭包会捕获附近作用域中的常量和变量,并使用强引用指向它们。你可以通过一个捕获列表来显示指定它的捕获行为。


捕获列表在参数列表之前,由中括号括起来,里面是由逗号分隔的一系列表达式。一旦使用了捕获列表,就必须使用in关键字,即使省略了参数名、参数类型和返回类型。


捕获列表中的项会在闭包创建时被初始化。每一项都会用闭包附近作用域中的同名常量或者变量的值初始化。例如下面的代码实例中,捕获列表包含a而不包含b,这将导致这两个变量有不同的行为。

var a = 0
var b = 0
let closure = { [a] in
print(a, b)
}

a = 10
b = 10
closure()
// 打印“0 10”

如果捕获列表中的值是类类型,可以使用weakunowned来修饰它,闭包会分别用弱引用、无主引用来捕获该值:

myFunction { print(self.title) }                    // 隐式强引用捕获
myFunction { [self] in print(self.title) } // 显式强引用捕获
myFunction { [weak self] in print(self!.title) } // 弱引用捕获
myFunction { [unowned self] in print(self.title) } // 无主引用捕获

在捕获列表中,也可以将任意表达式的值绑定到一个常量上。该表达式会在闭包被创建时进行求值,闭包会按照制定的引用类型来捕获表达式的值:

// 以弱引用捕获 self.parent 并赋值给 parent
myFunction { [weak parent = self.parent] in print(parent!.title) }

解决闭包的循环强引用


在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无助引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。


使用规则

  • 在闭包和捕获的实例总是互相引用并且同时销毁时,将闭包内的捕获定义为无主引用

  • 相反,在被捕获的引用可能会变为nil,将闭包内的捕获定义为弱引用,弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。这使我们可以在闭包体内检查它们是否存在


注意



如果被捕获的实例绝对不会变为nil,应该使用无主引用,而不是弱引用。



闭包是引用类型


无论你将函数和闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用


逃逸闭包


当一个闭包作为参数传到一个函数中,但是这个闭包在函数之后才被执行,称该闭包从函数中逃逸


在参数名之前标注@escaping指明这个闭包是允许逃逸出这个函数。


一种能使闭包"逃逸"出函数的方法是,将这个闭包包存在一个函数外部定义的变量中。例子:很多异步操作的函数接受一个闭包参数作为completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。这种情况下,闭包需要"逃逸"出函数,因为闭包需要在函数返回之后被调用:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}

注意



将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self



自动闭包


自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印“Now serving Ewa!”

总结


Swift 的闭包有以下几个主要的知识点:


  1. 闭包表达式(Closure Expressions):闭包表达式是一种在简短的几行代码中完成自包含的功能代码块。比如数组的排序方法 sorted(by:)
  2. 尾随闭包(Trailing Closures):如果你需要将一个很长的闭包表达式作为一个函数的最后一个参数,使用尾随闭包是很有用的。尾随闭包是一个书写在函数或方法的括号之后的闭包表达式。
  3. 值捕获(Value Capturing):闭包可以在其定义的上下文中捕获和存储任何常量和变量的引用。这就是所谓的闭包的值捕获特性。
  4. 闭包是引用类型(Closures Are Reference Types):无论你将函数/方法或闭包赋值给一个常量还是变量,你实际上都是将引用赋值给了一个常量或变量。如果你对这个引用进行了修改,那么它将影响原始数据。
  5. 逃逸闭包(Escaping Closures):一个闭包可以“逃逸”出被定义的函数并在函数返回后被调用。逃逸闭包通常存储在定义了该闭包的函数的外部。
  6. 自动闭包(Autoclosures):自动闭包能让你延迟处理,因为代码段不会被执行直到你调用这个闭包。自动闭包很有用,用来包装那些需要被延迟执行的代码。

Swift 闭包和OC Block


相似点:


  1. 都是可以捕获和存储其所在上下文的变量和常量的引用的代码块。
  2. 都可以作为参数传递给函数或方法,或者作为函数或方法的返回值。
  3. 都可以在代码块中定义局部变量和常量。
  4. 都可以访问其被创建时所处的上下文环境。

区别:


  1. 语法:Swift 的闭包语法更简洁明了,使用大括号 {} 来定义闭包,而 Objective-C 的 Block 语法相对复杂,使用 ^ 符号和大括号 ^{} 来定义 Block。
  2. 内存管理:Objective-C 的 Block 对捕获的对象默认使用强引用,需要注意避免循环引用;而 Swift 的闭包对捕获的变量默认使用强引用,但通过使用捕获列表(capture list)可以实现对捕获变量的弱引用或无引用。
  3. 类型推断:Swift 的闭包对于参数和返回值的类型具有类型推断的能力,可以省略类型注解;而 Objective-C 的 Block 需要明确指定参数和返回值的类型。
  4. 逃逸闭包:Swift 可以将闭包标记为 @escaping,表示闭包可能会在函数返回之后才被调用;而 Objective-C 的 Block 默认是可以在函数返回后被调用的。

作者:palpitation97
链接:https://juejin.cn/post/7250756790239969340
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

盘点那些国际知名黑客(上篇)

iOS
电影中的黑客仅靠一部电脑就可以窃取别人的信息,利用自己高超的技术让公司甚至国家都胆战心惊。“黑客”原指热心于计算机技术、水平高超的电脑高手,但逐渐区分为黑帽、白帽、灰帽。这些术语源自美国流行文化的老式西部电影,其中主角戴白色或浅色帽子,反派戴黑色帽子。黑帽黑客...
继续阅读 »

电影中的黑客仅靠一部电脑就可以窃取别人的信息,利用自己高超的技术让公司甚至国家都胆战心惊。“黑客”原指热心于计算机技术、水平高超的电脑高手,但逐渐区分为黑帽、白帽、灰帽。这些术语源自美国流行文化的老式西部电影,其中主角戴白色或浅色帽子,反派戴黑色帽子。

  • 黑帽黑客以“利欲”为目标,通过破解、入侵去获取不法利益或发泄负面情绪。
    • 灰帽黑客以“昭告”为目标,透过破解、入侵炫耀自己所拥有的高超技术。
    • 白帽黑客以“改善”为目标,破解某个程序作出修改,透过入侵去提醒设备的系统管理者其安全漏洞,有时甚至主动予以修补。


白帽黑客大多是电脑安全公司的雇员,抑或响应招测单位的悬赏,通常是在合法的情况下攻击某系统,而黑帽黑客同时也被称作“Cracker”(溃客),打着黑客的旗帜做不光彩的事情。接下来我们为大家介绍一下世界上非常厉害的顶级黑客。


“互联网之子”亚伦·斯沃茨 



2013 年 1 月 11 日,年仅26 岁的互联网奇才亚伦斯沃茨自杀身亡。他的一生都在为互联网的信息自由而努力。亚伦·斯沃茨 被称作计算机天才、互联网时代的普罗米修斯。但在这些光环的背后,是美国政府为他定下的 13 项重罪指控和最高 35 年的监禁。



1986年亚伦出生在一个程序员之家。3岁学会编程,12岁创建了一个知识共享网站,叫做 The info,功能和维基百科一样,但比维基百科早了 5 年。15岁参与制订了CC协议。18岁入学斯坦福,20岁辍学创业与Reddit项目的两位创始人合伙开公司,并创建了Reddit网站。 Reddit在当时的影响力不断扩大,成为最受欢迎的网站之一。后来,雅伦卖掉Reddit网站,赚了 100 万美元,在他 20 岁那年成为百万富翁。



亚伦参与构建了RSS,这是博客时代的工具,能让用户订阅自己感兴趣的博客,当订阅更新的时候,用户会收到邮件提醒。彼时的亚伦沉浸在互联网程序世界的理想主义美梦里,他希望自己能像他的偶像万维网的发明人蒂姆·博纳斯·李那样,让互联网回归自由、共享的初心。亚伦对赚钱并不感兴趣,他的梦想是追求一个更宏大的目标——互联网知识的自由和共享。



一次机会,亚纶了解到一个名为PACER的网站,它是一个存放法庭电子记录的系统,每看一页里面的内容,联邦政府需要收取 8 美分的管理费用。这项业务每年能带给政府超过 100亿美元的收入。亚纶认为,这些联邦法庭记录的材料本就属于公众,应当免费向公众开放。于是他编写了一个程序,抓取了超过 2000 万页的PACER资料,并将它们投放到公共资源网上,供大家免费阅读,这一举动相当于直接减少了美国司法系统200万美元的收入,PACER也在巨大的舆论压力下逐渐免费。



亚伦有一个“开放图书馆”的梦想,他认为实体的图书馆限制了知识的传播,而互联网是连接书籍、读者、作者、纸张与思想最好的载体。他在08年发表的《开放获取游击队宣言》中写道:信息就是力量,但就像所有力量一样,有些人只想将其占为己有。世界上大多数的期刊都被类似Elsevier、JSTOR这样的巨头垄断,每阅读一篇文献都需要支付一定数量的费用。亚伦想帮助更多的人平等地享受这些知识。于是他通过自己高超的黑客技术,利用麻省理工学院的校园网络免费端口从JSTOR下载了 480 万篇论文,相当于整个文献数据库的80% 。



亚伦毫无意外地被警察逮捕,但由于并未用论文牟利,JSTOR放弃了对他的指控。但马萨诸塞州检察长坚持起诉雅伦违反了1986年的计算机欺诈与滥用法。若罪名成立,亚伦将面临35年的监禁和100万美元的巨额罚款。亚伦拒绝认罪他选择与美国政府斗争。在这期间,他积极参与到各种推动知识共享的运动中,传播他关于知识共享的理念。



2012 年9月 12 日,联邦检察官提出了一份替换起诉书,增加了电子欺诈、非经授权访问计算机等罪名,从原来的 4 项重罪指控变成了 13 项。2013 年1月,雅伦在布鲁克林的公寓中上吊自杀,结束了自己的生命。这一年,他26岁。他死后,超过5万人在白宫网站上请愿,要是起诉亚伦的检察官辞职,维基百科以黑屏为他悼念。



亚伦认为知识共享能提高全人类的智慧,信息共享、言论自由才是真正的平等。在他死后,黑客入侵了麻省理工官网,抗议这个被视为黑客起源地的学府对于亚伦的无所作为。麻省理工的标题页被改为亚伦在2008 年写下的《开放获取游击队宣言》宣言中鼓励每一个网络用户行动起来,阻止商人与政客将网络私有化。



2013 年3月,亚伦被追授詹姆斯麦迪逊奖,用以表彰他捍卫公众的知情权所作出的贡献。


“世界头号黑客”凯文·米特尼克



凯文·米特尼克曾说:“巡游五角大楼,登录克里姆林宫,进出全球所有计算机系统,摧垮全球金融秩序和重建新的世界格局,谁也阻挡不了我们的进攻,我们才是世界的主宰。”



如果说谁的人生像小说一样精彩,那一定当属凯文·米特尼克。他出生于美国洛杉矶,是第一个被美国联邦调查局通缉的黑客,号称“世界头号黑客”。



20世纪80年代,他因多次入侵美国联邦调查局的中央电脑系统等而被逮捕三次。米特尼克的所作所为与人们所熟知的犯罪不同,他所做的一切似乎都不是为了钱,他曾破坏了40多家的安全系统,只是为了表明他“有能力做到”。



2000年,米特改邪归正,成为了一名白帽黑客,成功创办了米特尼克安全咨询公司,专门世界500强企业做网络咨询工作。2023年7月16日去世,享年59岁。


“C语言之父”丹尼斯·里奇



“丹尼斯·里奇一点也不家喻户晓,但是如果你有一台显微镜,能在电脑里看到他的作品,你会发现里面到处都是他的作品。”



丹尼斯·里奇(Dennis Ritchie)是美国计算机科学家,被称为“C语言之父”“Unix之父”。20世纪60年代,丹尼斯·里奇和肯·汤普逊参与了贝尔实验室Multics系统的开发。在开发期间,肯·汤普逊开发了游戏【空间旅行】,但当时的系统不给力,游戏运行速度很慢。



然而不久之后贝尔实验室撤出了Multics计划,里奇和汤普逊利用一台旧的迷你计算机Digital PDP-7,1969年的圣诞节Unix系统诞生了。最初的Unix内核使用B语言编写,为了更好开发Unix,1973年,里奇以B语言为基础发展出C语言,在它的主体设计完成后,他和汤普森又用它完全重写了Unix。



随着计算机的发展,编程语言层出不穷,但无论如何翻涌,都无法改变C语言在编程界德高望重的地位,C++、Java、C#都是在C语言的基础上衍生出来的。而如今诸多流行的操作系统也是在Unix的基础上开发的,如Linux、MacOS甚至最流行的手机系统Android。


丹尼斯·里奇发明的C语言联合Unix操作系统,构建了当代计算机世界的钢筋水泥。正是因为C语言和Unix系统这两项成就,里奇成为了许多编程爱好者膜拜的对象。


“Linux之父”林纳斯·托瓦兹



“Given enough eyeballs,all bugs are shallow.”【很多双眼睛盯着的代码,bug无处藏身】


1991年Linus开发了Linux操作系统,在最初几年里,Linux并没有得到太多关注。但随着互联网的普及,如今的linux已经成为全球最受欢迎的操作系统之一,被广泛应用于服务器、移动设备、家庭电脑和超级计算机等领域。



Linux的诞生充满了偶然,林纳斯经常用他的终端仿真器去访问大学主机上的新闻组和邮件,为了方便读写和下载文件,他自己编写了磁盘驱动程序和文件系统。这些在后来成为了Linux第一个内核的雏形,那时的他年仅21岁。



我们能够看到如今日渐壮大的Linux,但也不难发现,在成功的Linux背后,有着几十年如一日的持之以恒,有着对高质量代码的坚持,更是有着合作的。林纳斯没有建立组织,仅仅通过吸引全球数以万计的自由开发者免费贡献就完成了项目。Linux不仅仅是一个代码项目,也是一种互联网出现以后的新的协作方式——开源模式。


写在最后


现在国家很重视网络安全建设,网络安全已经成为了很多高校的一级学科,因此通过正常学习即可进入网络安全行业,大家一定要遵纪守法,效仿黑客们的行为做一些非法的黑客攻击行为,下期我们将继续为大家送上其他几位世界著名黑客的传奇故事,请大家保持关注哦。


作者:禅道程序猿
链接:https://juejin.cn/post/7273125596951478284
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

🔥🔥🔥996已明确违法,从此拒绝精神内耗!

之前一个禅道用户说,他在国外工作时主动加过两次班,然而被上司慰问了。上司特别严肃地跟他说:“请你不要再加班了,这让我很困扰。我们不加班,而且我无法向我的上司解释你为什么要加班,工作做不完可以明天做,工作只是你一天的一部分,利用好这8小时就可以了。” 对内卷严重...
继续阅读 »

之前一个禅道用户说,他在国外工作时主动加过两次班,然而被上司慰问了。上司特别严肃地跟他说:“请你不要再加班了,这让我很困扰。我们不加班,而且我无法向我的上司解释你为什么要加班,工作做不完可以明天做,工作只是你一天的一部分,利用好这8小时就可以了。”


对内卷严重的公司来说:一天干8小时怎么够?全天all in的状态才是我想要的。于是996疯狂盛行。


冷知识:“996”已严重违反法律规定。


早在2021年8月,最高法、人社部就曾联合发布超时加班典型案例,明确“工作时间为早9时至晚9时,每周工作6天”的内容,严重违反法律关于延长工作时间上限的规定,应认定为无效。


最近两会期间,全国政协委员蒋胜男也在提案中表示,应加强劳动法对劳动者的休息权保护。


由此,新的一波讨论已然来袭。


一、“996”带来了什么?



产品没有核心价值,缺乏核心竞争力,害怕落后于竞争激烈的市场……越来越多的管理者选择用加班、拉长工作时间来弥补技术创新的匮乏。


这种高强度的996工作制,侵占了我们的“充电”时间,甚至让我们丧失对新事物的接收力和思考能力;高强度的工作压力+长期的加班、熬夜、不规律饮食,给身体带来了沉重的负担;在忙碌了一周之后,感受到的是前所未有的迷茫与疲倦,精神内耗愈发严重


而对于企业来说,当员工沦为“执行工具”,原本的创新型发展却变成闭门造车,所以只能不停地加班、拉长工作时间,以产出更多的成果。长此以往,就形成了一种恶性循环。


在普遍“苦996久矣”的环境下,“8小时工作制”的推崇便显得尤为可贵。


二、“8小时工作制”从何而来?


8小时工作制,不应成为一个冷知识。《中华人民共和国劳动法》第三十六条规定:国家实行劳动者每日工作时间不超过8小时,平均每周工作时间不超过44小时的工时制度


8小时工作制的提出,要感谢来自英国的Robert Owen。1817年,他提出了“8小时工作制”,也就是将一天分成3等分,8小时工作、8小时娱乐、8小时休息。在当时一周普遍工作时间超过80个小时的情况下,这种要求简直是天方夜谭。


而8小时工作制得到推行,应归功于福特汽车品牌的创始人亨利·福特。1914年1月,福特公司宣布将员工的最低薪资从每天的2.34美元涨到5美元,工作时间减少至每天8小时。这项计划将会使福特公司多支付1000万美元。



在增加了员工薪资后,最直观的是员工流动率的下降。员工的稳定以及对操作的愈发熟练,增加了生产效率,从而降低成本、提高产量。最后,福特公司只用了两年时间,就将利润增加了一倍。


1926年,福特公司又宣布将员工的工作时间改为每周5天、每天8小时。亨利·福特用实际行动证明了增加工作收入、减少工作时间,对公司来说是可以实现正向创收的。


随后,8小时工作制才开始逐渐普及。随着Z时代的到来,更多新型职场状态也已经诞生。


液态职场早已到来,你准备好了吗?


三、液态职场是什么?



1)“3+2”混合办公模式


早在2022年,全国人大代表黄细花提交了建议,呼吁可推广“3+2”混合办公模式,允许员工每周可选择1-2天在家远程办公。黄细花还表示,推广“3+2”混合办公制,提高员工工作效率的同时,减轻年轻群体的生活压力,减少城市通勤压力。对女性员工而言,弹性的办公时间能让她们更好地平衡工作和生活。混合办公制对企业、员工和社会都将产生深远影响。


于是,不少企业开始了行动。携程推出了“3+2”混合办公模式的新政策:从 2022年3月起,允许员工每周三、周五在家远程办公。


2)四天半工作制


乐视也紧随其后,推出“四天半工作制”,每周三弹性工作半天。


3)“上4休3”的工作制


微软日本公司,也早在2019年8月曾宣布,公司开始试运行每周“上4休3”的工作制度,即每周五、六、日休息3天,周五所有办公室全部关闭。


不管是8小时工作制还是上4休3”,其实本质上都一样:都是为了迎合当下的现状,打破固有传统的工作模式,寻找更加多元化的新型职场状态,让员工能够充分休息,提升效率和创造力,也能节省企业开支,最终双方获益。


这世界变化太快了,上一秒还在“996”中疯狂内卷,下一秒就已经有先行者去探索更适合的工作节奏。液态职场时代已经到来,你准备好了吗?


四、提高工作效率,大胆对996说不!


作为打工人,不管是996还是8小时工作制,虽然都不是我们能决定的,但我们可以用法律来维护自己的权利,学会说“不”。利用好这8小时,发挥出自己的价值,提高自身的创新能力和效率,是为了更有底气的说“不”!这样才能保证企业与员工之间形成一个正向循环。如何利用好8小时?给大家分享几个提高工作效率的小技巧:

  1. 保持桌面整洁,减少其他事物对工作专注度的干扰;

  2. 巧用看板,可视化工作任务,便于进行任务管理;

  3. 排列优先级,按照任务的重要紧急程度,尽量避免并行多个任务;

  4. 随时记录工作中的创意和灵感

  5. 将重复、机械的工作自动化,解放双手;

  6. 定期复盘:不断改进与优化;

  7. 培养闭环思维:凡事有交代,件件有着落,事事有回音。


工作本应是我们热爱的样子。当我们还沉浸在无休止的工作与忙碌中,被疲惫、彷徨等负面情绪包围,开始精神内耗时,是时候明确拒绝996了!


作者:禅道程序猿
链接:https://juejin.cn/post/7217616698798096444
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS实现宽度不同无限轮播图

iOS
背景 项目中需要实现一个不同宽度的图片的无限轮播图效果,而且每次滚动,只滚到下一个图片。由于业界实现的轮播图效果都是等宽图片,所以需要重新根据“以假乱真”的原理,设计一款不同宽度的轮播效果; 演示效果 底部是个collectionView,顶部盖了个透明的sc...
继续阅读 »

背景


项目中需要实现一个不同宽度的图片的无限轮播图效果,而且每次滚动,只滚到下一个图片。由于业界实现的轮播图效果都是等宽图片,所以需要重新根据“以假乱真”的原理,设计一款不同宽度的轮播效果;


演示效果


底部是个collectionView,顶部盖了个透明的scrollView,传入的数据源是:

NSArray *imageWidthArray = @[@(200), @(60), @(120)];



实现思路


  1. 传入一个存储图片宽度的数组,计算出屏幕可见的个数,比如下图,假如可见数为3个;

  2. 左、右两侧各有2个灰块,用于实现以假乱真的数据;(两侧各需生成的灰块数=屏幕可见数-1)

    • 比如当前看到123,左滑会滚到231,再左滑会滚到312,此时设置contentOffset,切到前面那个312;
    • 比如当前看到123,右滑会滚到312,再右滑会滚到231,此时设置contentOffset,切到后面那个231;
    1. 为了性能方面的考虑,使用的是collectionView;
    2. 关于每次滚动,只滚到下一个,实现方式则是在collectionView上面盖一个scrollView,设置其isPagingEnabled = YES; scrollView里面的页数和数据源保持一致(方便计算滚到哪个page);





完整的代码实现


Github Demo


ViewController:

#import "ViewController.h"
#import "MyCollectionViewCell.h"

#define padding 10.f
#define margin 16.f
#define scrollViewWidth (self.view.bounds.size.width - 2 * margin)
#define scrollViewHeight 200.f

@interface ViewController ()<UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource>

@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) UIScrollView *topScrollView;
@property (nonatomic, strong) UIPageControl *pageControl;
@property (nonatomic, strong) NSArray *imageWidthArray; // 用户传入,图片宽度数组
@property (nonatomic, assign) NSInteger canSeeViewCount; // 屏幕最多可见几个view
@property (nonatomic, strong) NSMutableArray *imageWidthMuArray;
@property (nonatomic, strong) NSMutableArray *imageContentOffsetXArray;
@property (nonatomic, strong) NSMutableArray *currentPageMuArray;

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupViewWithImageWidthArray:@[@(200), @(60), @(120)]];
//    [self setupViewWithImageWidthArray:@[@(150), @(80),@(60), @(120)]];
}

-(void)setupViewWithImageWidthArray:(NSArray *)imageWidthArray {
    // 根据机型宽度,计算屏幕可见数量
    self.canSeeViewCount = imageWidthArray.count;
    CGFloat checkWidth = 0;
    for (NSInteger i = 0; i < imageWidthArray.count; i ++) {
        checkWidth += [imageWidthArray[i] floatValue];
        if (checkWidth >= scrollViewWidth) {
            self.canSeeViewCount = i + 1;
        }
    }

    self.imageWidthArray = imageWidthArray;
    self.imageContentOffsetXArray = [NSMutableArray arrayWithCapacity:self.imageWidthArray.count];

    // 插入头尾数据(前后插入可见数-1个)、生成currentPageMuArray
    self.imageWidthMuArray = [NSMutableArray array];
    self.currentPageMuArray = [NSMutableArray array];
    for (NSInteger i = self.imageWidthArray.count - (self.canSeeViewCount - 1); i < self.imageWidthArray.count; i ++) {
        [self.imageWidthMuArray addObject:self.imageWidthArray[i]];
        [self.currentPageMuArray addObject:@(i)];
    }
    [self.imageWidthMuArray addObjectsFromArray:self.imageWidthArray];

    for (NSInteger i = 0; i < self.imageWidthArray.count; i ++) {
        [self.currentPageMuArray addObject:@(i)];
    }

    for (NSInteger i = 0; i < (self.canSeeViewCount - 1); i ++) {
        [self.imageWidthMuArray addObject:self.imageWidthArray[i]];
        [self.currentPageMuArray addObject:@(i)];
    }

    CGFloat collectionViewContentSizeWidth = 0;
    for (NSInteger i = 0; i < self.imageWidthMuArray.count; i ++) {
        CGFloat imageWidth = [self.imageWidthMuArray[i] floatValue];
        if ( i > 0) {
            collectionViewContentSizeWidth += padding;
        }
        [self.imageContentOffsetXArray addObject:@(collectionViewContentSizeWidth)];
        collectionViewContentSizeWidth += imageWidth;
    }

    // collectionView
    UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
    flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    flowLayout.minimumInteritemSpacing = padding;
    flowLayout.minimumLineSpacing = padding;

    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(margin, 100, scrollViewWidth, scrollViewHeight) collectionViewLayout:flowLayout];
    [collectionView registerClass:[MyCollectionViewCell class] forCellWithReuseIdentifier:@"MyCollectionViewCell"];
    collectionView.dataSource = self;
    collectionView.delegate = self;
    collectionView.bounces = NO;
    collectionView.showsHorizontalScrollIndicator = NO;
    collectionView.backgroundColor = [UIColor brownColor];
    [self.view addSubview:collectionView];
    collectionView.contentSize = CGSizeMake(collectionViewContentSizeWidth, 0);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [collectionView setContentOffset:CGPointMake([self.imageContentOffsetXArray[self.canSeeViewCount - 1] floatValue], 0)];
    });
    self.collectionView = collectionView;

    // topScrollView
    UIScrollView *topScrollView = [[UIScrollView alloc] initWithFrame:collectionView.frame];
    topScrollView.showsHorizontalScrollIndicator = NO;
    [topScrollView setPagingEnabled:YES];
    topScrollView.backgroundColor = [UIColor clearColor];
    topScrollView.delegate = self;
    topScrollView.bounces = NO;
    [self.view addSubview:topScrollView];
    self.topScrollView = topScrollView;
    topScrollView.contentSize = CGSizeMake(self.imageWidthMuArray.count * scrollViewWidth, 0);
    [topScrollView setContentOffset:CGPointMake((self.canSeeViewCount - 1) * scrollViewWidth, 0)];

    // pageControl
    CGFloat pageControlHeight = 50.f;
    UIPageControl *pageControl = [[UIPageControl alloc] initWithFrame:CGRectMake(margin, 100 + scrollViewHeight-pageControlHeight, scrollViewWidth, pageControlHeight)];
    pageControl.numberOfPages = self.imageWidthArray.count;
    pageControl.currentPage = 0;
    [self.view addSubview:pageControl];
    self.pageControl = pageControl;
}

#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView == self.collectionView) {
        return;
    }

    // 页面整数部分
    NSInteger floorPageIndex = floor(scrollView.contentOffset.x / scrollView.frame.size.width);

    // 小数部分
    CGFloat pageRate = scrollView.contentOffset.x / scrollView.frame.size.width - floor(scrollView.contentOffset.x / scrollView.frame.size.width);
    CGFloat imageContentOffsetX = [self.imageContentOffsetXArray[floorPageIndex] floatValue];
    CGFloat imageWidth = [self.imageWidthMuArray[floorPageIndex] floatValue];
    self.collectionView.contentOffset = CGPointMake(imageContentOffsetX + (imageWidth + 10.f) * pageRate, 0);
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    NSInteger rightIndex = (self.canSeeViewCount - 1) + (self.imageWidthArray.count) - 1;
    NSInteger leftIndex = (self.canSeeViewCount - 1) - 1;

    // 右边卡到尾时
    if (self.collectionView.contentOffset.x == [self.imageContentOffsetXArray[rightIndex] floatValue]) {
        [self.collectionView setContentOffset:CGPointMake([self.imageContentOffsetXArray[leftIndex] floatValue], 0)];
    }

    // 左边卡到头时
    else if (self.collectionView.contentOffset.x == 0) {
        [self.collectionView setContentOffset:CGPointMake([self.imageContentOffsetXArray[self.imageWidthArray.count] floatValue], 0)];
    }

    // 右边卡到尾时
    if (self.topScrollView.contentOffset.x == scrollViewWidth * rightIndex) {
        [self.topScrollView setContentOffset:CGPointMake(scrollViewWidth * leftIndex, 0)];
    }

    // 左边卡到头时
    if (self.topScrollView.contentOffset.x == 0) {
        [self.topScrollView setContentOffset:CGPointMake(scrollViewWidth * self.imageWidthArray.count, 0)];
    }

    // 设置currentPage
    NSInteger floorPageIndex = floor(scrollView.contentOffset.x / scrollView.frame.size.width);
    self.pageControl.currentPage = [self.currentPageMuArray[floorPageIndex] intValue];
}

#pragma mark - UICollectionViewDelegate, UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return self.imageWidthMuArray.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MyCollectionViewCell" forIndexPath:indexPath];
    cell.labelText = [NSString stringWithFormat:@"%.0f", [self.imageWidthMuArray[indexPath.item] floatValue]];
    return cell;
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    return CGSizeMake([self.imageWidthMuArray[indexPath.item] floatValue], scrollViewHeight);
}

@end

MyCollectionViewCell:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface MyCollectionViewCell : UICollectionViewCell

@property (nonatomic, copy) NSString *labelText;

@end

NS_ASSUME_NONNULL_END
#import "MyCollectionViewCell.h"
#import "Masonry.h"

@interface MyCollectionViewCell()

@property (nonatomic, strong) UILabel *label;

@end

@implementation MyCollectionViewCell

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setupUI];
    }
    return self;
}

- (void)setupUI {
self.backgroundColor = [UIColor grayColor];
    UILabel *label = [[UILabel alloc] init];
    label.textAlignment = NSTextAlignmentCenter;
    label.font = [UIFont boldSystemFontOfSize:18];
    [self.contentView addSubview:label];
    [label mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.contentView);
    }];
    self.label = label;
}

- (void)setLabelText:(NSString *)labelText {
    _labelText = labelText;
    self.label.text = labelText;
}

-(void)prepareForReuse {
    [super prepareForReuse];
    self.label.text = @"";
}
@end

作者:Wiley_Wan
链接:https://juejin.cn/post/7231443152212312123
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

环信FCM推送详细步骤

集成FCM推推送
准备的地址有 :https://firebase.google.com
1.firebase官网选择我们自己创建的项目

2.点到这个设置按键

3.我们打开到项目设置->常规 拉到最下面有一个“您的应用” 点击下载json文件,json文件的使用是客户端放在安卓项目的app目录下

4.首先环信需要的信息有 项目设置中-> 服务账号 生成新的私钥 生成的文件我们要上传到环信的管理后台证书部分(V1)

5.点击上传证书会选择你下载的文件,注意!! 名称是由你设置的项目名称的json文件 并不是 google-services.json
6.项目名称 是你的发送者ID 这个id 我们在firebase官网中的项目设置-〉常规 -〉您的项目->的项目编号就是您的SenderID 填写到环信官网即可 另外客户端的 google-services.json 这个文件 打开后 project number 也是SenderID

7.将我们下载好的 google-services.json 文件放到app的目录下 (文件获取可以反回步骤3 查看)

8.打开build的根目录添加 :
buildscript {
dependencies {
// classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.google.gms:google-services:4.3.8'
}
}

9.build.gradle.app部分添加:
implementation platform('com.google.firebase:firebase-bom:28.4.1')
implementation 'com.google.firebase:firebase-messaging'

10.对应好appkey 以及我们的客户端初始化fcm的senderID

11.在登陆前 初始化以后 添加以下代码:
EMPushHelper.getInstance().setPushListener(new PushListener() {
@Override
public void onError(EMPushType pushType, long errorCode) {
EMLog.e("PushClient", "Push client occur a error: " + pushType + " - " + errorCode);
}

@Override
public boolean isSupportPush(EMPushType pushType, EMPushConfig pushConfig) {
if(pushType==EMPushType.FCM)
{
return GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(MainActivity.this)
== ConnectionResult.SUCCESS;
}
return super.isSupportPush(pushType, pushConfig);
}
});

12.登陆成功后的第一个页面添加 :
if(GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(NewAcitivty.this) != ConnectionResult.SUCCESS) {
return;
}
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (!task.isSuccessful()) {
EMLog.d("PushClient", "Fetching FCM registration token failed:"+task.getException());
return;
}
// 获取新的 FCM 注册 token
String token = task.getResult();
EMClient.getInstance().sendFCMTokenToServer(token);
}
});

13.清单文件注册sevices 主要是为了继承FCM的服务 必要操作!

添加代码: 重写onMessageReceived
收到消息后 就在这个方法中 自己调用 本地通知 因为fCM的推送只有唤醒
public class FireBaseservice extends FirebaseMessagingService {
@Override
public void onMessageReceived(@NonNull RemoteMessage message) {
super.onMessageReceived(message);
if (message.getData().size() > 0) {
String alter = message.getData().get("alter");
Log.d("", "onMessageReceived: " + alter);
}

}
@Override
public void onNewToken(@NonNull String token) {
Log.i("MessagingService", "onNewToken: " + token);
// 若要对该应用实例发送消息或管理服务端的应用订阅,将 FCM 注册 token 发送至你的应用服务器。
if(EMClient.getInstance().isSdkInited()) {
EMClient.getInstance().sendFCMTokenToServer(token);
}
}
}
14.准备测试 这个时候我们就要验证我们的成果了 首先要看自己登录到环信后 是否有绑定证书 借用环信的即时推送功能查看是否有绑定证书
这个时候看到登录了证书还是没有绑定上 那肯定是客户端出现问题了

15.检查错误 看到提示了com.xxxx.play 安装 这个是因为 你的设备没有打开 VPN 或者VPN不稳定,所以你首先要确定VPN打开并且 稳定 然后我们在重新登录测试

16.这个时候我们在借用即时推送查看 看看有没有绑定到环信 看到该字样就证明你的证书已经绑定上了 直接杀掉进程离线 测试离线推送,(一定要在清单文件注册的谷歌服务中 重新的onMessageReceived 中写入本地通知展示 不然fcm的推送只有唤醒)

升级Xcode 15后,出现大量Duplicate symbols问题的解决方案

升级到Xcode 15后,原先Xcode14可以编译的项目出现大量Duplicate symbols,且引用报错指向同一个路径(一般为Framework)下的同一个文件。经过查找相关资料,查到可通过在Xcode -> Target -> Build...
继续阅读 »

升级到Xcode 15后,原先Xcode14可以编译的项目出现大量Duplicate symbols,且引用报错指向同一个路径(一般为Framework)下的同一个文件。经过查找相关资料,查到可通过

在Xcode -> Target -> Build Setting -> Other Linker Flags 添加一行"-ld64"

即可解决该问题

原因是Xcode15采用了新的链接器(Linker),被称作“ld_prime”。新的连接器有诸多好处,尤其是对合并库的支持方面,具体可以查看WWDC 2023 SESSION 10268 Meet mergeable libraries.。然而,链接器的升级可能会出现不兼容老库的情况出现。遇到这种情况,可以通过恢复旧的连接器来解决这个问题。从Other Linker Flags添加"-ld64"后,就会覆盖Xcode编译时选择的链接器,因此可以正常访问。

收起阅读 »