Salesforce Open Badges:由 CRM 触发器颁发可验证凭证

通过 Apex 调用 Badges Ninja API,让任意 Salesforce 对象事件——Lead 赢单、Account 升级、培训里程碑——自动触发 Open Badge 颁发。

Nacho Coll 作者 更新于 14 分钟阅读
通过 Apex 调用 Badges Ninja API,让任意 Salesforce 对象事件——Lead 赢单、Account 升级、培训里程碑——自动触发 Open Badge 颁发。

大多数培训团队早已在 Salesforce 里记录了谁完成了什么。课程报名状态变为 “Completed”,合作伙伴账户达到某个认证等级,客户完成了入门引导。记录变了,然后一周后有人导出名单,手动用邮件发送证书。

本指南教你补上这个缺口:当记录达到你关心的状态时,Salesforce 调用 https://badges.ninja 的 API,颁发一份 Open Badge v2.0 凭证,并把验证链接回写到该记录上。之后你可以把这个链接放进邮件模板、报表或 Experience Cloud 页面。

你需要一位 Salesforce 管理员(或一个开发者沙盒)、一枚已在 https://badges.ninja 上设计好的数字徽章,以及一个 API 密钥。如果你从未调用过这个 API,请先阅读 API 快速入门,只需十分钟。

什么时候适合用 CRM 触发器

在写 Apex 之前,先确认你是否真的需要它。如果你每学期或每季度批量颁发一次凭证,仪表板的 CSV 上传更快,而且完全不需要代码。Salesforce 方案在以下情况下才物有所值:

  • 记录一变更就应立即颁发,无需任何人工步骤。
  • “谁获得了凭证”的数据本来就在 Salesforce 里,你不想再维护第二个数据源。
  • 你希望把凭证链接存放在 Contact、Lead 或自定义培训记录上,以便做报表。

如果你的工作流工具是 Zapier 而不是 Apex,同样的 API 调用在那里也能用;参见 Zapier Open Badges 自动化。

你要构建什么

整个流程由四部分组成:

  1. 一枚徽章,在可视化设计器中设计。复制它的 id(即 GET /badges 返回的 id)。
  2. 一个 API 密钥,在仪表板的 API Keys 页面创建。密钥只显示一次,所以请立刻保存。
  3. 一个 Named Credential,位于 Salesforce 中,用来存放密钥,使其永远不会出现在代码里。
  4. 一个 Apex 触发器加一个 Queueable 类,负责颁发凭证,并把颁发链接写回记录。

Newly created API key revealed once

第 1 步:把密钥存进 Named Credential

在 Apex 中硬编码 API 密钥是必须避免的错误。在 Setup 中,先创建一个带自定义请求头的 External Credential,再创建一个指向 API 主机的 Named Credential:

  • URL: https://api.badges.ninja
  • 自定义请求头名称: X-Api-Key
  • 自定义请求头值: 你的密钥,格式为 bws_YOUR_KEY_HERE
  • Named Credential 名称: BadgesNinja

请让运行自动化的权限集(permission set)拥有访问该 External Credential 主体(principal)的权限。有了 Named Credential,你就不需要单独的 Remote Site Setting;Salesforce 会通过该凭证授权调用。如果你的组织仍在使用旧版 Named Credential 或普通端点,请把 https://api.badges.ninja 添加为 Remote Site Setting,并在代码中发送请求头。

第 2 步:为记录添加字段

在代表成就的对象上(我们使用一个名为 Training_Enrollment__c 的自定义对象),添加:

  • Badge_Id__c(Text):要颁发的徽章。按课程存放它,可以让一个触发器服务多枚徽章。
  • Badge_Award_Id__c(Text,unique):API 返回的颁发 id。
  • Badge_Url__c(URL):公开的验证页面。
  • Badge_Error__c(Long Text):最近一次错误,这样管理员无需打开调试日志就能看到失败原因。

Badge_Award_Id__c 字段一物两用。它是你的防重复保护:只有当它为空时,触发器才会颁发。

第 3 步:Apex 调用

触发器不能直接发起调用,因为在存在未提交 DML 的同一事务中不允许进行 callout。请把工作交给实现了 Database.AllowsCallouts 的 Queueable。

POST /awards 的 API 约定很简短。请求体包含 badgeId、一个嵌套的 recipient 对象(带 name 和 email),以及必填的 issuedOn。调用成功时返回 HTTP 200 和 { "awardId": "..." }。接收者姓名至少要有五个字符,因此名为 “Al Li” 的 Contact 会被拒绝;调用前请先校验。

public class BadgeIssueJob implements Queueable, Database.AllowsCallouts {
    private Id enrollmentId;
    public BadgeIssueJob(Id enrollmentId) { this.enrollmentId = enrollmentId; }

    public void execute(QueueableContext ctx) {
        Training_Enrollment__c e = [
            SELECT Id, Badge_Id__c, Badge_Award_Id__c, Contact__r.Name, Contact__r.Email
            FROM Training_Enrollment__c WHERE Id = :enrollmentId LIMIT 1
        ];
        if (e.Badge_Award_Id__c != null) { return; }   // already issued

        Map<String, Object> recipient = new Map<String, Object>{
            'name' => e.Contact__r.Name,
            'email' => e.Contact__r.Email
        };
        Map<String, Object> body = new Map<String, Object>{
            'badgeId' => e.Badge_Id__c,
            'recipient' => recipient,
            'issuedOn' => Datetime.now().getTime()
        };

        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:BadgesNinja/awards');
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(JSON.serialize(body));

        HttpResponse res = new Http().send(req);
        if (res.getStatusCode() == 200) {
            Map<String, Object> out = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
            String awardId = (String) out.get('awardId');
            e.Badge_Award_Id__c = awardId;
            e.Badge_Url__c = 'https://badges.ninja/awards/' + awardId;
            e.Badge_Error__c = null;
        } else {
            e.Badge_Error__c = res.getStatusCode() + ': ' + res.getBody();
        }
        update e;
    }
}

有两个细节值得注意。第一,issuedOn 是纪元毫秒数,这正是 Datetime.getTime() 返回的值。第二,响应中不包含验证 URL,所以代码用颁发 id 自行拼出:https://badges.ninja/awards/<awardId>。

第 4 步:触发器

让触发器保持精简。它只负责判断哪些记录符合条件,并为每条记录入队一个任务。

trigger TrainingEnrollmentBadge on Training_Enrollment__c (after insert, after update) {
    for (Training_Enrollment__c e : Trigger.new) {
        Boolean completed = e.Status__c == 'Completed';
        Boolean wasCompleted = Trigger.isUpdate && Trigger.oldMap.get(e.Id).Status__c == 'Completed';
        if (completed && !wasCompleted && e.Badge_Award_Id__c == null && e.Badge_Id__c != null) {
            System.enqueueJob(new BadgeIssueJob(e.Id));
        }
    }
}

wasCompleted 这个判断很关键。没有它,对已完成记录的任何后续编辑都会再次触发逻辑。再加上任务内部”颁发 id 为空”的保护,一条记录最多只会产生一份凭证。

还有一个需要遵守的限制:Salesforce 只允许在单个事务中入队少量 Queueable 任务(同步上下文中为 50 个)。如果你批量导入完成记录,比如一次导入 2000 行数据,上面的触发器就会撞上这个上限。对于批量加载,要么改用仪表板的 CSV 批量颁发来上传名单,要么改用带 Database.AllowsCallouts 的 Batchable 类,每个 scope 只处理少量记录。

API keys list showing the masked prefix

错误处理与重试

API 返回标准状态码,每一种都需要你的代码做出不同的响应:

状态码含义处理方式
400字段缺失或无效,包括接收者姓名少于五个字符不要重试。修正数据,并把消息写入 Badge_Error__c。
401API 密钥错误或缺失检查 Named Credential。在修复之前重试没有用。
402你的套餐凭证数量或配额已用完停止颁发并通知管理员;升级套餐或配额重置后再重试。
404badgeId 不存在修正记录或课程上的 Badge_Id__c。
5xx 或超时暂时性故障可以稍后安全重试。

由于任务会把错误写到记录上,并让 Badge_Award_Id__c 保持为空,重试就像勾选一个复选框或重新保存记录一样简单。再写一个小型的定时 Apex 类,查找已完成但没有颁发 id 且带有暂时性错误的报名记录并重新入队,你就拥有了一个无需额外基础设施的自动重试队列。请用计数字段限制尝试次数,避免永久出错的记录无限循环。

接入生产环境之前,请先确认你的套餐。Free 套餐每年包含 1,200 份凭证;Starter 为 $9/月,Pro 为 $29/月。如果触发器对每个 Lead 都触发,小套餐很快就会被耗尽,所以请把条件限定在真正的成就上。

让凭证在 Salesforce 中可见

Badge_Url__c 一旦有值,你就可以在任何地方使用它:

  • 邮件提醒与模板: 把该字段合并进一封”恭喜,这是你的凭证”邮件。
  • 报表: 基于 Badge_Award_Id__c != null 的报表可以按课程、地区或账户负责人统计颁发数量。
  • Experience Cloud: 在合作伙伴的认证页面显示该链接,让买家自行验证。
  • 页面布局: 添加该 URL 字段,让支持人员一眼就能回答”她拿到数字徽章了吗?”

验证页面是公开的,无需注册,这正是它的意义所在。接收者、招聘经理或客户的采购团队都可以打开链接,确认是谁颁发了这份凭证、它证明了什么以及颁发时间。如需机器可读的验证,任一颁发记录的 Open Badge v2.0 JSON-LD 可通过 https://api.badges.ninja/certify-badge/award/<awardId> 获取。

接收者还能获得常见的附加功能:唯一的验证 URL、二维码、A4 PDF 证书,以及接收者门户中的”添加到 LinkedIn”选项,该门户通过魔法链接而非密码登录。

为什么这比 AppExchange 徽章插件更划算

AppExchange 上的徽章插件通常按 Salesforce 席位或托管包许可证计费。这笔成本随你的 Salesforce 团队规模增长,而不是随你颁发的凭证数量增长,因此十人的培训团队和一万名学员的项目,为同样的产出付出的费用差别很大。

API 集成则反转了这一模式。你按凭证数量以固定套餐付费,触发器代码可以自由阅读和修改,输出是标准的 Open Badge v2.0,接收者不会被锁定在专有的查看器里。你也保留了设计自由:可视化设计器提供 80 多种模板、形状、图标和自定义上传,而不是软件包内固定的版式。

坦率地说,这里有个取舍:插件点击即可安装,不需要 Apex。如果团队里没有人能维护几十行代码,插件或无代码连接器可能更合适。如果你比较的是整体平台而不是集成方式,Credly 替代方案一文并排对比了价格和功能差异。

上线前的测试

先在沙盒中完整跑一遍流程。

  1. 创建一枚测试徽章,以及一个使用你可控邮箱地址的测试接收者。
  2. 在 Apex 测试中,实现 HttpCalloutMock,返回状态码 200 和 {"awardId":"test-award-id"};Salesforce 不允许在测试上下文中发起真实 callout,而你需要这部分覆盖率才能部署。
  3. 在沙盒中,把一条报名记录设为 Completed,并检查 Badge_Award_Id__c 和 Badge_Url__c 是否在一分钟内被填充。
  4. 在无痕浏览器窗口中打开该 URL,确认公开验证页面无需登录即可显示。
  5. 人为制造一次失败(清空徽章 id,或使用少于五个字符的接收者姓名),并确认错误出现在 Badge_Error__c 中。

如果 API 密钥曾泄露到调试日志或截图中,请立即轮换。在 API Keys 页面可以即时吊销,而你只需更新 External Credential,无需改动代码。

总结

这个模式很小巧:精简的触发器决定何时颁发,Queueable 负责调用,Named Credential 保管密钥,记录上的三个字段保存结果。它适用于培训里程碑、合作伙伴等级、活动出席,或任何你能在 Salesforce 中表达的状态变化。

从一个对象和一枚徽章开始,观察一周,等重试行为符合你的数据后再扩展。

准备好颁发你的第一份可验证凭证了吗? 在 badges.ninja 免费开始 — 可视化设计器、公开验证页面、PDF 证书、Open Badge v2.0 输出。无需信用卡。

Nacho Coll

关于作者

Founder & Engineer at Badges Ninja

Nacho founded Badges Ninja to make issuing verifiable digital credentials as simple as a single API call — Open Badge v2.0 badges and certificates, minted, hosted, and verifiable without standing up your own issuer infrastructure. Writes about the Open Badges spec, credential verification, and running a credentialing platform serverless on AWS, from the operator side of the wire.

返回博客

相关文章