当前位置: 首页 > news >正文

Hyperf方案 飞书消息卡片交互 - 发送可交互的消息卡片(按钮/下拉框),用户点击后回调到 Hyperf 服务处理业务

整体流程 构建卡片 JSON → 发送卡片消息 → 用户点击按钮 → 飞书回调 → 验签 → 状态机处理 → 更新卡片---1.卡片构建器 app/Service/Feishu/Card/CardBuilder.php<?phpnamespaceApp\Service\Feishu\Card;classCardBuilder{privatearray $elements=[];privatearray $header=[];publicfunctionheader(string $title,string $color='blue'):static{// color: blue | green | red | yellow | grey | orange | purple | indigo | wathet | turquoise | carmine$this->header=['title'=>['tag'=>'plain_text','content'=>$title],'template'=>$color,];return$this;}publicfunctiontext(string $content,bool$markdown=false):static{$this->elements[]=['tag'=>'div','text'=>['tag'=>$markdown?'lark_md':'plain_text','content'=>$content,],];return$this;}publicfunctionfields(array $kvPairs):static{// 两列布局展示键值对$this->elements[]=['tag'=>'div','fields'=>array_map(fn($k,$v)=>['is_short'=>true,'text'=>['tag'=>'lark_md','content'=>"**{$k}**\n{$v}"],],array_keys($kvPairs),$kvPairs),];return$this;}publicfunctiondivider():static{$this->elements[]=['tag'=>'hr'];return$this;}/** * 按钮行 * $buttons = [ * ['text' => '同意', 'action' => 'approve', 'type' => 'primary', 'value' => ['id' => 1]], * ['text' => '拒绝', 'action' => 'reject', 'type' => 'danger', 'value' => ['id' => 1]], * ] */publicfunctionbuttons(array $buttons):static{$this->elements[]=['tag'=>'action','actions'=>array_map(fn($btn)=>['tag'=>'button','text'=>['tag'=>'plain_text','content'=>$btn['text']],'type'=>$btn['type']??'default',// primary | danger | default'value'=>array_merge(['action'=>$btn['action']],$btn['value']??[]),],$buttons),];return$this;}/** * 下拉选择框 */publicfunctionselect(string $placeholder,string $action,array $options):static{$this->elements[]=['tag'=>'action','actions'=>[['tag'=>'select_static','placeholder'=>['tag'=>'plain_text','content'=>$placeholder],'value'=>['action'=>$action],'options'=>array_map(fn($label,$val)=>['text'=>['tag'=>'plain_text','content'=>$label],'value'=>(string)$val,],array_keys($options),$options),]],];return$this;}/** * 输入框(飞书卡片 2.0) */publicfunctioninput(string $placeholder,string $action,string $name='input'):static{$this->elements[]=['tag'=>'action','actions'=>[['tag'=>'input','placeholder'=>['tag'=>'plain_text','content'=>$placeholder],'value'=>['action'=>$action,'name'=>$name],]],];return$this;}publicfunctionbuild():array{return['schema'=>'2.0','header'=>$this->header,'body'=>['elements'=>$this->elements],];}publicfunctiontoJson():string{returnjson_encode($this->build(),JSON_UNESCAPED_UNICODE);}}---2.卡片发送(复用 MessageService) app/Service/Feishu/CardService.php<?phpnamespaceApp\Service\Feishu;use App\Service\Feishu\Card\CardBuilder;classCardService{publicfunction__construct(privateMessageService $messageService,privateFeishuClient $client){}/** * 发送卡片消息,返回 message_id(用于后续更新卡片) */publicfunctionsend(string $receiveId,CardBuilder $card,string $idType='open_id'):string{$data=$this->client->post("/im/v1/messages?receive_id_type={$idType}",['receive_id'=>$receiveId,'msg_type'=>'interactive','content'=>$card->toJson(),]);return$data['data']['message_id'];}/** * 更新已发送的卡片内容(用户点击后刷新卡片状态) */publicfunctionupdate(string $messageId,CardBuilder $card):void{$this->client->post("/im/v1/messages/{$messageId}/patch",['content'=>$card->toJson(),]);}/** * 回调中直接响应更新卡片(无需 message_id,飞书推荐方式) */publicfunctionbuildCallbackResponse(CardBuilder $card):array{return['toast'=>['type'=>'success','content'=>'操作成功'],'card'=>$card->build(),];}}---3.回调验签 app/Service/Feishu/CardVerifier.php<?phpnamespaceApp\Service\Feishu;use Hyperf\Contract\ConfigInterface;classCardVerifier{publicfunction__construct(privateConfigInterface $config){}/** * 飞书卡片回调验签 * 签名规则:timestamp + "\n" + token + "\n" + body → sha1 */publicfunctionverify(string $timestamp,string $nonce,string $signature,string $body):bool{$token=$this->config->get('feishu.verification_token');$expect=sha1($timestamp."\n".$nonce."\n".$token."\n".$body);returnhash_equals($expect,$signature);}}---4.状态机 app/Service/Feishu/Card/CardActionStateMachine.php<?phpnamespaceApp\Service\Feishu\Card;use App\Service\Feishu\Card\Handler\ApproveHandler;use App\Service\Feishu\Card\Handler\RejectHandler;use App\Service\Feishu\Card\Handler\AssignHandler;classCardActionStateMachine{// action => [允许的当前状态列表]privateconstTRANSITIONS=['approve'=>['pending'],'reject'=>['pending'],'assign'=>['pending','processing'],'cancel'=>['pending','processing'],];privatearray $handlers;publicfunction__construct(ApproveHandler $approve,RejectHandler $reject,AssignHandler $assign,){$this->handlers=['approve'=>$approve,'reject'=>$reject,'assign'=>$assign,];}/** * 处理卡片动作 * @return CardBuilder 返回更新后的卡片 */publicfunctionhandle(string $action,string $currentStatus,array $value,array $operator):CardBuilder{$allowed=self::TRANSITIONS[$action]??[];if(!in_array($currentStatus,$allowed,true)){return$this->buildErrorCard("当前状态「{$currentStatus}」不允许执行「{$action}」");}$handler=$this->handlers[$action]??null;if(!$handler){return$this->buildErrorCard("未知操作: {$action}");}return$handler->handle($value,$operator);}privatefunctionbuildErrorCard(string $msg):CardBuilder{return(newCardBuilder())->header('操作失败','red')->text($msg);}}---5.Action Handler 示例 app/Service/Feishu/Card/Handler/ApproveHandler.php<?phpnamespaceApp\Service\Feishu\Card\Handler;use App\Model\Order;use App\Service\Feishu\Card\CardBuilder;classApproveHandler{publicfunctionhandle(array $value,array $operator):CardBuilder{$orderId=$value['id'];$order=Order::findOrFail($orderId);$order->update(['status'=>'approved','approved_by'=>$operator['open_id'],'approved_at'=>date('Y-m-d H:i:s'),]);// 返回更新后的卡片(替换原卡片内容)return(newCardBuilder())->header('审批完成','green')->fields(['订单号'=>$order->order_no,'状态'=>'已通过','审批人'=>$operator['name'],'时间'=>date('Y-m-d H:i:s'),])->text('> 审批已完成,无法再次操作',true);}}---6.回调 Controller app/Controller/Feishu/CardCallbackController.php<?phpnamespaceApp\Controller\Feishu;use App\Model\Order;use App\Service\Feishu\CardVerifier;use App\Service\Feishu\Card\CardBuilder;use App\Service\Feishu\Card\CardActionStateMachine;use Hyperf\HttpServer\Annotation\Controller;use Hyperf\HttpServer\Annotation\PostMapping;use Hyperf\HttpServer\Contract\RequestInterface;#[Controller(prefix:'/feishu/card')]classCardCallbackController{publicfunction__construct(privateCardVerifier $verifier,privateCardActionStateMachine $stateMachine){}#[PostMapping(path:'/callback')]publicfunctioncallback(RequestInterface $request):array{$body=$request->getBody()->getContents();$timestamp=$request->getHeaderLine('X-Lark-Request-Timestamp');$nonce=$request->getHeaderLine('X-Lark-Request-Nonce');$signature=$request->getHeaderLine('X-Lark-Signature');// 验签if(!$this->verifier->verify($timestamp,$nonce,$signature,$body)){return['code'=>403,'msg'=>'验签失败'];}$payload=json_decode($body,true);// 飞书握手if(isset($payload['challenge'])){return['challenge'=>$payload['challenge']];}// 解析动作$action=$payload['action']['value']['action']??'';$value=$payload['action']['value']??[];$operator=['open_id'=>$payload['operator']['open_id']??'','name'=>$payload['operator']['name']??'',];// 下拉框选中值在 option 字段if($payload['action']['tag']==='select_static'){$value['selected']=$payload['action']['option']??'';}// 查当前状态$orderId=$value['id']??null;$currentStatus=$orderId?Order::find($orderId)?->status:'unknown';// 状态机处理,返回新卡片$newCard=$this->stateMachine->handle($action,$currentStatus,$value,$operator);// 直接在回调响应中更新卡片(飞书推荐,无需额外 API 调用)return['toast'=>['type'=>'success','content'=>'操作成功'],'card'=>$newCard->build(),];}}---7.使用示例// 发送审批卡片$card=(newCardBuilder())->header('待审批订单','blue')->fields(['订单号'=>'ORD-2026-001','申请人'=>'张三','金额'=>'¥ 3,200.00','事由'=>'采购办公用品',])->divider()->select('指派给...','assign',['李四'=>'ou_aaaaaa','王五'=>'ou_bbbbbb',])->buttons([['text'=>'同意','action'=>'approve','type'=>'primary','value'=>['id'=>1]],['text'=>'拒绝','action'=>'reject','type'=>'danger','value'=>['id'=>1]],]);$messageId=$this->cardService->send('ou_xxxxxxxx',$card);// 存 message_id,后续可主动更新卡片Order::find(1)->update(['feishu_message_id'=>$messageId]);---关键点汇总 ┌──────────────────┬───────────────────────────────────────────────────────────┐ │ 要点 │ 说明 │ ├──────────────────┼───────────────────────────────────────────────────────────┤ │ 验签方式 │ 卡片回调用 sha1,事件回调用 AES,两者不同 │ ├──────────────────┼───────────────────────────────────────────────────────────┤ │ 回调响应更新卡片 │ 直接在响应体返回 card 字段,比调 patch API 更快 │ ├──────────────────┼───────────────────────────────────────────────────────────┤ │ toast 提示 │ type 支持 success/error/info,给用户即时反馈 │ ├──────────────────┼───────────────────────────────────────────────────────────┤ │ 防重复点击 │ 状态机校验当前状态,已处理的单子直接返回错误卡片 │ ├──────────────────┼───────────────────────────────────────────────────────────┤ │ 下拉框取值 │ 选中值在 action.option,不在 action.value │ ├──────────────────┼───────────────────────────────────────────────────────────┤ │ schema2.0│ 新版卡片必须声明"schema":"2.0",否则部分组件不生效 │ ├──────────────────┼───────────────────────────────────────────────────────────┤ │3秒超时 │ 回调必须3秒内响应,复杂业务用队列异步,响应先返回 toast │ └──────────────────┴───────────────────────────────────────────────────────────┘
http://www.jsqmd.com/news/622922/

相关文章:

  • DeOldify图像上色服务:快速修复老旧照片,色彩自然还原
  • GitHub中文界面插件完整指南:一键实现全平台中文化
  • 盘点纸飞机艺术岛的优势在哪里,苏州热门户外休闲园区推荐 - myqiye
  • 3步突破Windows 11硬件限制:老旧电脑升级完整方案
  • 2026年纺织品市场测评报告:头部面料供应商能力拆解与选型指南 - 2026年企业推荐榜
  • 3大核心功能揭秘:HSTracker如何让macOS炉石玩家实现智能决策优化
  • 忍者像素绘卷微信小程序离线能力:Service Worker缓存像素模板
  • HTML标签必须闭合吗_哪些标签可以省略闭合【解答】
  • WarcraftHelper终极指南:3步解决魔兽争霸III兼容性问题
  • GLM-OCR AI编程辅助工具:自动生成代码注释与文档
  • 3大核心功能解析:ArchivePasswordTestTool高效恢复加密压缩包密码
  • 企业级Multi-Agent落地的组织变革:从职能型到项目制的重构
  • Topit:重塑数字注意力流,Mac端智能视觉层管理终极方案
  • AI元人文:当算法假装读懂了孟子——论学术辅助中的认知骗局与阅读主体的失落
  • 腾讯混元翻译模型完整教程:Hunyuan-MT-7B部署、调用、优化全流程
  • 别再只问价格了!2026年铝合金防静电地板选购指南:这3个参数比便宜更重要
  • 佳木斯冬季装修,墙面材料怎么选
  • MySQL分区实战指南:从原理到落地的完整攻略
  • 春联生成模型-中文-base效果实测:输入‘平安‘、‘富贵‘,对联寓意满满
  • LingBot-Depth部署案例:边缘AI盒子(如Lantern、Neuralet)适配记录
  • 终极魔兽争霸3兼容性修复指南:5个简单步骤彻底告别闪退
  • 2017年计算机二级C语言测试题及答案解析
  • 终极指南:NormalMap-Online - 浏览器本地GPU加速的法线贴图生成工具
  • 说说灶福星家宴猛火灶,节能省气效果如何,在上海靠谱吗? - 工业品牌热点
  • 探讨长沙前三特产正宗的米酒,承源百年古酒馆性价比如何? - 工业推荐榜
  • sa-token学习(2)
  • 分析灶福星家用猛火灶,上海北京等地用户反馈怎么样? - myqiye
  • PP-DocLayoutV3在金融文档处理中的创新应用:财报表格区域自动裁剪与结构化提取
  • AIOPS运维学习计划第1周时间安排
  • 嵌入式c语言——关键字5