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

张开发
2026/4/11 10:11:48 15 分钟阅读

分享文章

Hyperf方案 飞书消息卡片交互 - 发送可交互的消息卡片(按钮/下拉框),用户点击后回调到 Hyperf 服务处理业务
整体流程 构建卡片 JSON → 发送卡片消息 → 用户点击按钮 → 飞书回调 → 验签 → 状态机处理 → 更新卡片---1.卡片构建器 app/Service/Feishu/Card/CardBuilder.php?phpnamespaceApp\Service\Feishu\Card;classCardBuilder{privatearray $elements[];privatearray $header[];publicfunctionheader(string $title,string $colorblue):static{// color: blue | green | red | yellow | grey | orange | purple | indigo | wathet | turquoise | carmine$this-header[title[tagplain_text,content$title],template$color,];return$this;}publicfunctiontext(string $content,bool$markdownfalse):static{$this-elements[][tagdiv,text[tag$markdown?lark_md:plain_text,content$content,],];return$this;}publicfunctionfields(array $kvPairs):static{// 两列布局展示键值对$this-elements[][tagdiv,fieldsarray_map(fn($k,$v)[is_shorttrue,text[taglark_md,content**{$k}**\n{$v}],],array_keys($kvPairs),$kvPairs),];return$this;}publicfunctiondivider():static{$this-elements[][taghr];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[][tagaction,actionsarray_map(fn($btn)[tagbutton,text[tagplain_text,content$btn[text]],type$btn[type]??default,// primary | danger | defaultvaluearray_merge([action$btn[action]],$btn[value]??[]),],$buttons),];return$this;}/** * 下拉选择框 */publicfunctionselect(string $placeholder,string $action,array $options):static{$this-elements[][tagaction,actions[[tagselect_static,placeholder[tagplain_text,content$placeholder],value[action$action],optionsarray_map(fn($label,$val)[text[tagplain_text,content$label],value(string)$val,],array_keys($options),$options),]],];return$this;}/** * 输入框飞书卡片 2.0 */publicfunctioninput(string $placeholder,string $action,string $nameinput):static{$this-elements[][tagaction,actions[[taginput,placeholder[tagplain_text,content$placeholder],value[action$action,name$name],]],];return$this;}publicfunctionbuild():array{return[schema2.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 $idTypeopen_id):string{$data$this-client-post(/im/v1/messages?receive_id_type{$idType},[receive_id$receiveId,msg_typeinteractive,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[typesuccess,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);$expectsha1($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{$allowedself::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];$orderOrder::findOrFail($orderId);$order-update([statusapproved,approved_by$operator[open_id],approved_atdate(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[code403,msg验签失败];}$payloadjson_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[typesuccess,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同意,actionapprove,typeprimary,value[id1]],[text拒绝,actionreject,typedanger,value[id1]],]);$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 │ └──────────────────┴───────────────────────────────────────────────────────────┘

更多文章