作者: 夏晚
Salesforce的SplashPage(启动页)-一天一个标准功能系列
最近有小盆友考验我,说客户想点进Tab的时候先进入一个Guide页面,然后点击继续进入list页面,点击Don’t show again下次不再显示此页面,直接进入List页面。
问我该怎么做。
我反问,”你想怎么做?”
“首先呢,建一个Tab”
“废话。哪种Tab。”
“当然是Visualforce Tabs。”
“然后呢。”
“然后我创建一个VF,并且使用标准标签和样式。”
“嗯,为了让用户觉得自己没跳进VF是么?”
“没错,然后我在VF里选中用户点的Tab,之后在页面里写上我要的内容。”
“这个简单,那俩按钮你想怎么实现呢?”
“当然是建两个Button,然后在Controller里实现两个Action。一个Button直接跳转到List页面。另一个Button。。。。”
“Don’t show again button你想怎么实现呢?”
“我在User表上加一个字段,如果用户点了Don’t show again,我就更新一下User身上的Flag,然后在VF初始化的时候就判断User身上的Flag,如果是true就直接跳转。”
“唔。。。可行,你觉得复杂么?”
“还好吧。”
“听说过Splash Page么?”
“啥?”
Splash Page,启动页,醒目页面。是Salesforce提供的标准功能之一。
其作用是在Tab上添加一个启动页面,并且原生支持Don’t show again。
其效果如图

点击Tab之后,会显示指定的内容,并提供Don’t show again与Continue按钮。
不过,我发现只有Custom Object的Tab才可以指定Splash Page,标准Object的Tab不可以。
可能Salesforce觉得标准Object应该不会像Custom Object那样复杂到需要启动页来介绍功能吧。
那么接下来介绍该如何创建一个启动页。
首先,你需要创建Custom Link,此Custom Link为Home的Custom Link,而不是该Custom Object上的Custom Link。

创建的过程与创建button或者link相同,种类有三种,JS,VF或者URL。

然后随便创建一个VF,在此选择。
值得注意的是,一定要记得关闭标准标签和标准样式,否则就会出现像下面这样奇怪的事情。

关闭标准标签标准样式的写法。
<apex:page showheader="false" standardStylesheets="false"> ...... </apex:page>
Custom Link设定好之后,去Tab的设置页面,指定这个Custom Link,大功告成。

Salesforce作为云计算平台,提供了强大的自定义开发能力。但是,现代应用开发思想是轻开发,重配置。常年从事SI的前JAVA从业者,很容易就陷入开发解决一切的思维黑洞。付出高昂的成本反复造轮子,因为其价值也只是在反复造轮子上了。利用标准功能快速的实现客户需求,才是未来的发展方向。大型开发的市场会一直存在,但将将来绝对不会是主流。
Salesforce中Picklist与翻译的故事
不同与Java系统,salesforce给Select或者Drop down,whatever你叫它什么的那个元素提供了一个很好的组织方式———-Picklist。
Picklist作为一种基本数据类型,在salesforce里任何人都可以使用其轻松的创建下拉列表,并动态的控制其中的选项。要知道,在java系统里,为了达到这个目的,可是要上三大框架的。如果不能用自定义标签库,那么在不hard coding的前提下,在画面上可控的显示一个select对于一个新手来讲,将是一个堪比穿越撒哈拉沙漠的挑战。当然,大部分项目都选择直接写死,或者使用Js操纵DOM。
不过Salesforce的Picklist与我们印象中的Select元素还是不同的。
在Spring 17之前(Spring 17 release note),picklist的值只有一个value,所以表示文本也是它,值也是它。所以好多系统就顺水推舟的在代码,validation rule等地方直接用长长的value内容做判断。
突然有一天,客户想对选项的内容做一个小小的改动。。。。。灾难了。
以前的话,更改选项的值,既存数据是不会跟着变的,要手动修数据。现在已经能够做到既存的数据自动跟着变了。但是那些代码里写死的判断等就不行了。
我们知道,html中的select是要分别设定value和label的!在页面表示label,表单里传的是value。这样根据定义好的数据字典,在程序里随便判断value。无论画面表示的本文怎么改都不会影响到程序。
所以sfdc的先贤们发明了一个绝招————翻译。
比如picklist的一个项目的value是1,则在翻译控制台将1翻译成想显示的文本,比如“A”。那么就实现了HTML的Select的特性—–显示的文本和后台传值分开。
正当群众们洋洋得意的时候,Spring17来了。给我们带来了picklist选项的Api Name,参见官方博客。
抱歉,说的不严谨,应该是给我们带来了Picklist选项的Label。
就是说现在Picklist的选项有两个设定值值,一个是Label,一个是Api Name。Api Name其实还是我们以前熟悉的那个选项value,老朋友。新增加的是Label。
那么好玩的地方来了。以前Picklist选项只有一个value的时候,翻译就直接那么翻译了。当Label和Api Name分开之后,翻译是针对谁来进行翻译呢?
这里直接公布答案—–是Label。
可以用下面的代码进行尝试。
设定好翻译之后
List<Schema.PicklistEntry> picklistValues = SomeSobject__c.SomePicklistField__c.getDescribe().getPicklistValues();
for (Schema.PicklistEntry pe: picklistValues) {
system.debug('PicklistEntry Value:' + pe.getValue() + ' Translated Label:' + pe.getLabel());
}
value既Api Name,Label既Label
这样,我们是可以做到在Apex中获取Picklist选项的Label,而且获得的还是翻译后的Label。
可曾记起我们还有Formula呢,isPickVal函数判断的又是谁呢—————没错,还是老朋友,Api Name。
目前我还没有找到如何在formula里获取Label名的方法。
Salesforce怎么从生产环境删除Apex
一般来讲,就算是DevOps团队也很难遇到需要删除Apex的情景。
毕竟在一个系统的生命周期里,大部分时间都是不断的增加功能,或者改动现有功能。
仅剩的一点点的遗弃功能的需求,也会因为考虑到风险问题,从而选择将废弃代码放在那里不再调用。
毕竟,维护团队的准则————不动就没事,动了就要担风险。
其实,还有一种需要删除Apex操作的情景,就是回滚。
Salesforce与常规的Java项目不大一样,如果上线出现问题,很难做到完美回滚。
曾经有人很天真地和我说,将生产环境的MetaData全部备份,如果上线出问题就全都deploy回去。
java web系统确实可以这么做,一个新版war包,一个旧war包,出问题就停机覆盖回去,一切就都恢复如初了,完美。
可惜,如果Salesforce这么做就会面临一场巨大的灾难!
首先,你有很大的几率会彻彻底底的失败。因为Salesforce在deploy的时候要计算metadata之间的关系。
你会因为各种狗追尾巴似的关系依赖,导致失败,继而崩溃。
还有一些永远无法被正确Deploy上去的metadata,会将你的生产环境毁成一片废墟。
在Salesforce里,只要是稍微复杂一点的Org,就永远不要幻想将全部的Metadata都一口气deploy上去。
话说回来,Salesforce最安全的回滚方式还是逆操作。就像录像带的倒放。
但是,就算是逆操作,仍然不能保证安全的,原原本本的回到从前,尤其是回滚包含修复数据动作的时候。
所以在上线之前,针对回滚,认认真真地制定一个回滚方案是非常,非常有必要的。
当然,如果你每次上线只是加一个Field,当我没说。
不过,就算是只删一个Field的话,慎重吧。
回到正题,怎么去删除一个Apex。
有的同学说了。直接删就好了啊。
那么。。。只接触过Developer环境和Sandbox的可以出门右转了。生产环境是无法直接删除的。
那么,下面,先讲一个简单的方法。
首先,我们有一个ApexClass,叫DeleteSample。

然后在ForceIDE中,新建一个Project,将其MetaData取下。

之后打开此Apex,之后切换到Metadata标签上。

然后将Status从Active改成Deleted


然后,保存,在Apex上点右键,Force.com -> Deploy to Server。

之后填入用户名密码,下一步,强调一下,被删除的Apex会被标记成Overwrite,且默认不被选中。如果是新建的代码则会被标记成New,绿色,自动勾选。修改的代码被标记成Modify,黄色,自动勾选。这里,一定要记得钩选上。

然后无脑下一步,直到看到Success。

然后回到系统里,就发现DeleteSample已经不见了!

就这样。简单吧。
Salesforce用Apex判断Role Hierarchy的简单代码示例
(上密码遭人恨。。。。先开放好了)
由于role不同于Profile,带有阶层性质,所以有一些自定义功能要依赖于这种阶层的设定。这样就涉及到role hierarchy的判断问题。
我是一个绝懒之人,所以去网上搜了一下,能找到的方案都或多或少有些缺陷 。
我所提供的方案也是如此,但是想比于浪费太多SOQL查询次数来讲,role的数量不超过50000条已经是足够好了。
// 这里Update一下,其实根本不会有那么多的Role,因为默认500,提票才能达到10000。
Talk is cheap, show you the code.
PS:最近正在建设个人代码库,本身也只是一个简单的示例,之后会放出完全体版本。
也许也会放到Github上。
// @Version 0.1 Author Keal. Email: [email protected] // @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License public class RoleHierarchyHelper { Boolean isEnd = false; Boolean compareResult = false; public static List<UserRole> roleList = [SELECT Id, Name, DeveloperName, ParentRoleId FROM UserRole LIMIT 50000]; public RoleHierarchyHelper() { // pretend Salesforce spasms LOL. It happened. if(roleList == null || roleList.size() == 0) { roleList = [SELECT Id, Name, DeveloperName, ParentRoleId FROM UserRole LIMIT 50000]; } else { system.debug('=======RoleList.size()========' + roleList.size()); } } public Boolean isSubordinate(String currentRoleId, String compareRoleId){ List<UserRole> childRoleList = new List<UserRole>(); Map<Id, UserRole> childRoleMap = new Map<Id, UserRole>(); Set<Id> exeUserRoleIdSet = new Set<Id>(); exeUserRoleIdSet.add(currentRoleId); isEnd = false; compareResult = false; while(!isEnd) { childRoleMap = new Map<Id, UserRole>(); for(UserRole ur: roleList) { if(ur.ParentRoleId != null && exeUserRoleIdSet.contains(ur.ParentRoleId)) { childRoleMap.put(ur.Id, ur); } } if(childRoleMap.size() > 0) { if(childRoleMap.containsKey(compareRoleId)) { isEnd = true; compareResult = true; } else { isEnd = false; exeUserRoleIdSet = new Set<Id>(); exeUserRoleIdSet.addAll(childRoleMap.keyset()); } } else { isEnd = true; compareResult = false; } } return compareResult; } public Boolean isSuperior(String currentRoleId, String compareRoleId) { Map<Id, UserRole> idUserRoleMap = new Map<Id, UserRole>(roleList); Id exeUserRoleId = currentRoleId; UserRole exeUserRole = new UserRole(); isEnd = false; compareResult = false; while(!isEnd) { exeUserRole = idUserRoleMap.get(exeUserRoleId); if(exeUserRole.parentRoleId == null) { isEnd = true; compareResult = false; } else { if(exeUserRole.parentRoleId == compareRoleId) { isEnd = true; compareResult = true; } else { isEnd = false; exeUserRoleId = exeUserRole.parentRoleId; } } } return compareResult; } // TODO isSameLine() }
.user.ini无法编辑的可能原因
今天给我的WP安装了Wordfence插件(人气安全插件),因为读书笔记用的Ngnix,所以并没有php.ini,只有.user.ini。好死不死插件要求更改此文件。
然后因为连我都无法编辑此文件,导致插件死活都开启不了防火墙。
然后我尝试了后台/在线编辑IDE/SSH,均无法更改权限到可编辑。
然后去了宝塔的后台安全管理溜达一圈,也没看到有关于这个文件的设定。
最后灵机一动,莫非。。。chattr。
果然,执行chattr -i .user.ini之后,权限就可以编辑了。插件功能顺利开启。
之后将权限重新设定成644,chattr +i .user.ini,打完收工。
那么chattr命令与.user.ini都是做什么的呢?之后补充。
Salesforce中避免重复执行Trigger的思路
题外话,换了效力的公司之后,对自己的冲击还是挺大的。所以一直没有精力继续写。不过,还是一鼓作气吧。
第一种思路:在trigger中声明一个Static变量,默认为false,如果执行了业务代码,在最后将此变量值执为True,然后在一个事务中,下次再进来的时候,由于此变量为true,跳过。
但是由于static的机制并不是很稳定,有可能造成意想不到的问题。而且,在早些年版本的时候,一个批次200条数据,salesforce会分两次执行。这样后100条就会被无情的跳过。
第二种思路:在目标object上新建一个隐藏的布尔字段,默认为false,第一次执行的时候将此字段的值更新成true,然后在事务结束的时候使用future方法重新更新为false。
超大量导入数据的时候应该会死。。。
第三种思路:新建一个static class,class里面包含一个set,然后每次事务开始时将自己放入此set,事务结束时remove掉。
仍然有static诡异行为或者直接失效的风险。
所以,在表设计和业务结构设计的时候还是要尽量发生重复触发的情景。让Trigger只关注与自己要做的事情就好。
之后会给出每种思路的验证结果。
// Update 1 by 2017/03/04
应群众要求,解释一下思路一。
思路一的核心就在于static修饰符。官方文档。
熟悉高级编程语言的同学一定对static不陌生,比如在java中static经常与public final static连用。
Salesforce同样提供了static概念,不过于java中的static有些不同。
1. static的生存范围仅限一个transaction。所以幻想着初始化一次到处使用的,洗洗睡吧。
2. 虽然不能做到亘古不变,但是在一个transaction内穿梭于多个instance之间还是可以的。
同样,可以在class中的static代码块中进行复杂的初始化,也是能避免重复执行。
所以作为思路一的解决方案。就如同官方文档提供的例子一样。
首先找一个class声明一个flag。
public class P {
public static boolean firstRun = true;
}
然后在业务代码里使用它。
trigger T1 on Account (before delete, after delete, after undelete) {
if(Trigger.isBefore){
if(Trigger.isDelete){
if(p.firstRun){
Trigger.old[0].addError('Before Account Delete Error');
p.firstRun=false;
}
}
}
}
密码保护:Salesforce关于Person Account与Contact Role
搬瓦工启用chacha20加密算法的方法
搬瓦工的一键SS默认的加密算法是AES-256,这个算法在当时算是兼具了安全和性能的不二选择。
可是随着时代的发展,AES-256的性能已经不能满足广大人民群众的需求。所以新一代的算法冉冉升起————–chacha20
chacha20虽然名字很奇怪,但确是Google出品。来自RFC与Wikipedia的介绍。
ChaCha20
Google选择了带有Bernstein的Poly1305消息认证码的ChaCha20作为一个OpenSSL中RC4的替代品,用以完成互联网的安全通信。[17]Google最初实现了https (TLS/SSL)流量在Chrome浏览器(Android手机版)与Google网站之间的通信。[18]
不久之后,Google在TLS中采用它,ChaCha20和Poly1305算法也以 [email protected] 成为OpenSSH中的一个新密码包。[19][20]后来,通过编译时选项避免它依赖于OpenSSL也成为可能。[21]
ChaCha20也被用在OpenBSD[22]和NetBSD[23]操作系统中的arc4random随机数生成器,替换已经脆弱的RC4,在DragonFly BSD[24]中内核的CSPRNG子程序中也是如此。[25][26]
ChaCha20已经在RFC 7539中标准化。它在IKE和IPsec中的使用已在RFC 7634中标准化。在 RFC7905中,Chacha20-Poly1305 已经被加入 TLS 扩展标准([1])。
chacha20的优势在于移动设备的性能。所以我们开始动手改造吧。
按照以下步骤:
首先SSH连接自己的搬瓦工服务器。
标准起手动作,yum update。
然后执行下面命令。
yum install m2crypto gcc -y wget https://github.com/jedisct1/libsodium/releases/download/1.0.8/libsodium-1.0.8.tar.gz tar zfvx libsodium-1.0.8.tar.gz cd libsodium-1.0.8 ./configure make && make install echo "include ld.so.conf.d/*.conf" > /etc/ld.so.conf echo "/lib" >> /etc/ld.so.conf echo "/usr/lib64" >> /etc/ld.so.conf echo "/usr/local/lib" >> /etc/ld.so.conf ldconfig
因为我还没找到搬瓦工自带SS的配置文件位置。所以只能用临时的办法。
ps -ef | grep ssserver // 获得 sssever PID kill [PID] /usr/bin/ssserver -p [your port] -k [your password] -m chacha20 --user nobody
之后在客户端也选择chacha20就OK了。
操作完之后,虽然搬瓦工自带的SS管理页面看起来是STOP状态,但实际上服务器已经重新启动了。
等找到配置文件的位置之后,再更新比较完善的方法。
// Update 1 2017-02-08
关于如何做到开机自动启动与自定义参数的问题,已经找到了方案。
首先,在/etc目录下创建配置文件shadowsocks.json(vi /etc/shadowsocks.json或者ftp等你喜爱的方式)
然后输入下列内容
{
"server":"[Server IP]",
"local_address":"127.0.0.1",
"local_port":1080,
"port_password":{
"[Port]":"[Password]"
},
"timeout":300,
"method":"chacha20",
"fast_open":false
}
然后,打开/etc/rc.local,将原来的ssserver启动命令删掉,然后写入
ssserver-c /etc/shadowsocks.json -d start
之后重启,连接成功就OK了。
以后想改密码或者端口的话,直接改/etc/shadowsocks.json中的设置,重启就好。
// Update 2 2017-06-18
今天发现所有使用chacha20加密的流量被服务器疯狂的拒绝。然后监视服务器侧并没有收到任何请求,推测已被拦截。
为了追求速度快就会留下马脚。所以请暂且使用AES-256吧
// Update 2 2017-09-08
更新了tar包的url地址
如何在Salesforce中使用匿名块建立每小时执行一次的Scheduler
在Salesforce中建立Scheduler的方式有两种
- 在Develop->Apex Classes页面,点击Schedule Apex按钮之后会提供如下的面板。

这个面板虽然能方便的决定Scheduler执行的频率,但是最高频率也只能是一天一次。 - 再有就是使用Apex。在匿名块中使用系统函数设定Scheduler,可以获得更高的频率。
所以,如果想设定一小时执行的Scheduler就必须使用第二种方式。
需要使用的函数为System.schedule();
官方文档对此进行了详细的描述。具体参照下列示例代码。
// Sample 1 hour 1 time
System.schedule('My Scheduler','0 0 0/1 * * ?', new MyScheduler());
作为一个成熟的完善的系统,定时器与后台Job是必须的组成部分。
Scheduler就是Linux Cron的强力山寨。
所以System.schedule()中的第二个参数,就等同于Cron表达式(当然,只能算一个子集)。
为什么要把一小时一次单独拎出来,因为,这是Scheduler的最小粒度。
Scheduler表达式的秒位与分位是不支持“/”符号的。
所以,想几分钟执行一次的,放弃吧。