欢迎光临
我们一直在努力

自动化“联系我们”:纯前端接入飞书群机器人消息通知实践

自动化“联系我们”:纯前端接入飞书群机器人消息通知实践

1. 背景与目标

在官网“联系我们”页面里,用户线索如果不能第一时间触达业务群,常见问题就是响应慢、跟进晚、丢线索。

本文聚焦一个纯前端项目里的可落地方案:

  • 用户提交联系表单
  • 前端完成校验与消息组装
  • 直接调用飞书群机器人 Webhook
  • 群内实时收到线索通知

本文不涉及后端接口实现。


2. 功能效果展示

在这里插入图片描述

在这里插入图片描述

用户体验路径:

  • 用户在 Contact 页面填写姓名、邮箱、电话、咨询方向、需求描述
  • 点击提交后立即进行前端校验
  • 校验通过后直接请求飞书群机器人 Webhook
  • 发送成功跳转 /contact-success,发送失败展示错误提示

  • 3. 技术选型与方案说明(纯前端)

    3.1 本文方案

    当前项目采用的是:前端直连飞书群机器人 Webhook。

    优点:

    • 接入成本低
    • 链路短,联调快
    • 适合官网线索通知这类轻量场景

    边界:

    • Webhook 相关配置在前端侧存在暴露风险
    • 风控能力有限(需依赖飞书机器人安全设置)

    3.2 接入前置条件(你必须先完成)

  • 企业先为开发人员开通飞书开发者权限。
  • 登录飞书开放平台创建应用,并为应用添加能力:机器人。
  • 按飞书官方教程在目标群创建群机器人,获取 Webhook 链接。
  • 本项目中,代码保存的是 FEISHU_WEBHOOK_KEY,由代码拼接成完整 URL:
    https://open.feishu.cn/open-apis/bot/v2/hook/<REDACTED>


    4. 项目结构与关键文件说明

    本次链路相关文件:

    lada/
    ├─ src/components/Contact/ContactForm.vue
    ├─ src/config/feishu.js
    ├─ src/config/apiKey.js # 本地密钥文件(被 .gitignore 忽略)
    └─ .gitignore

    关键职责:

    文件作用
    lada/src/components/Contact/ContactForm.vue 表单渲染、字段校验、提交、错误提示、调用飞书发送逻辑
    lada/src/config/feishu.js 飞书配置、消息模板函数、消息体构造工具
    lada/src/config/apiKey.js 存放高德与飞书 key(本地文件,未纳入版本控制)
    lada/.gitignore 明确忽略 /src/config/apiKey.js

    代码扫描结论:

    • 项目内未发现 /api/contact、/message/notify、/feishu/webhook 等后端实现
    • 当前是纯前端直连飞书群机器人 Webhook

    5. 核心链路解析(含 Mermaid 图)

    5.1 调用链路清单

    环节实现
    触发点 用户点击提交按钮,触发表单 @submit.prevent="handleSubmit"
    前端模块与函数 ContactForm.vue:handleSubmit() -> validateForm() -> sendToFeishu()
    网络请求路径与方法 POST https://example.com/open-apis/bot/v2/hook/<REDACTED>
    后端路由与处理函数 无(纯前端)
    飞书消息发送实现 feishu.js:formatContactMessage() + buildMessage()
    成功分支 返回 code === 0 后,约 1 秒跳转 /contact-success
    失败分支 catch 异常,设置 messageStatus 并显示“提交失败”文案

    5.2 总体架构图

    #mermaid-svg-2cXZphShnIy51Lsg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2cXZphShnIy51Lsg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2cXZphShnIy51Lsg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2cXZphShnIy51Lsg .error-icon{fill:#552222;}#mermaid-svg-2cXZphShnIy51Lsg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2cXZphShnIy51Lsg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2cXZphShnIy51Lsg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2cXZphShnIy51Lsg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2cXZphShnIy51Lsg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2cXZphShnIy51Lsg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2cXZphShnIy51Lsg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2cXZphShnIy51Lsg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2cXZphShnIy51Lsg .marker.cross{stroke:#333333;}#mermaid-svg-2cXZphShnIy51Lsg svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2cXZphShnIy51Lsg p{margin:0;}#mermaid-svg-2cXZphShnIy51Lsg .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-2cXZphShnIy51Lsg .cluster-label text{fill:#333;}#mermaid-svg-2cXZphShnIy51Lsg .cluster-label span{color:#333;}#mermaid-svg-2cXZphShnIy51Lsg .cluster-label span p{background-color:transparent;}#mermaid-svg-2cXZphShnIy51Lsg .label text,#mermaid-svg-2cXZphShnIy51Lsg span{fill:#333;color:#333;}#mermaid-svg-2cXZphShnIy51Lsg .node rect,#mermaid-svg-2cXZphShnIy51Lsg .node circle,#mermaid-svg-2cXZphShnIy51Lsg .node ellipse,#mermaid-svg-2cXZphShnIy51Lsg .node polygon,#mermaid-svg-2cXZphShnIy51Lsg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2cXZphShnIy51Lsg .rough-node .label text,#mermaid-svg-2cXZphShnIy51Lsg .node .label text,#mermaid-svg-2cXZphShnIy51Lsg .image-shape .label,#mermaid-svg-2cXZphShnIy51Lsg .icon-shape .label{text-anchor:middle;}#mermaid-svg-2cXZphShnIy51Lsg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2cXZphShnIy51Lsg .rough-node .label,#mermaid-svg-2cXZphShnIy51Lsg .node .label,#mermaid-svg-2cXZphShnIy51Lsg .image-shape .label,#mermaid-svg-2cXZphShnIy51Lsg .icon-shape .label{text-align:center;}#mermaid-svg-2cXZphShnIy51Lsg .node.clickable{cursor:pointer;}#mermaid-svg-2cXZphShnIy51Lsg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2cXZphShnIy51Lsg .arrowheadPath{fill:#333333;}#mermaid-svg-2cXZphShnIy51Lsg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2cXZphShnIy51Lsg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2cXZphShnIy51Lsg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2cXZphShnIy51Lsg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2cXZphShnIy51Lsg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2cXZphShnIy51Lsg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2cXZphShnIy51Lsg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2cXZphShnIy51Lsg .cluster text{fill:#333;}#mermaid-svg-2cXZphShnIy51Lsg .cluster span{color:#333;}#mermaid-svg-2cXZphShnIy51Lsg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2cXZphShnIy51Lsg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2cXZphShnIy51Lsg rect.text{fill:none;stroke-width:0;}#mermaid-svg-2cXZphShnIy51Lsg .icon-shape,#mermaid-svg-2cXZphShnIy51Lsg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2cXZphShnIy51Lsg .icon-shape p,#mermaid-svg-2cXZphShnIy51Lsg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2cXZphShnIy51Lsg .icon-shape rect,#mermaid-svg-2cXZphShnIy51Lsg .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2cXZphShnIy51Lsg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2cXZphShnIy51Lsg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2cXZphShnIy51Lsg :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    POST

    用户浏览器

    ContactForm.vue

    validateForm

    sendToFeishu

    FeishuUtils.formatContactMessage

    apiKey.js: FEISHU_WEBHOOK_KEY=

    Feishu Group Bot Webhook

    飞书群

    5.3 时序图

    飞书群飞书群机器人WebhookFeishuUtilsContactForm.vue飞书群飞书群机器人WebhookFeishuUtilsContactForm.vue#mermaid-svg-fSqk1RuZhrZqfgkT{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fSqk1RuZhrZqfgkT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fSqk1RuZhrZqfgkT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fSqk1RuZhrZqfgkT .error-icon{fill:#552222;}#mermaid-svg-fSqk1RuZhrZqfgkT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fSqk1RuZhrZqfgkT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fSqk1RuZhrZqfgkT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fSqk1RuZhrZqfgkT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fSqk1RuZhrZqfgkT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fSqk1RuZhrZqfgkT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fSqk1RuZhrZqfgkT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fSqk1RuZhrZqfgkT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fSqk1RuZhrZqfgkT .marker.cross{stroke:#333333;}#mermaid-svg-fSqk1RuZhrZqfgkT svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fSqk1RuZhrZqfgkT p{margin:0;}#mermaid-svg-fSqk1RuZhrZqfgkT .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fSqk1RuZhrZqfgkT text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-fSqk1RuZhrZqfgkT .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fSqk1RuZhrZqfgkT .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-fSqk1RuZhrZqfgkT .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-fSqk1RuZhrZqfgkT .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-fSqk1RuZhrZqfgkT #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-fSqk1RuZhrZqfgkT .sequenceNumber{fill:white;}#mermaid-svg-fSqk1RuZhrZqfgkT #sequencenumber{fill:#333;}#mermaid-svg-fSqk1RuZhrZqfgkT #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-fSqk1RuZhrZqfgkT .messageText{fill:#333;stroke:none;}#mermaid-svg-fSqk1RuZhrZqfgkT .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fSqk1RuZhrZqfgkT .labelText,#mermaid-svg-fSqk1RuZhrZqfgkT .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-fSqk1RuZhrZqfgkT .loopText,#mermaid-svg-fSqk1RuZhrZqfgkT .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-fSqk1RuZhrZqfgkT .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fSqk1RuZhrZqfgkT .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-fSqk1RuZhrZqfgkT .noteText,#mermaid-svg-fSqk1RuZhrZqfgkT .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-fSqk1RuZhrZqfgkT .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fSqk1RuZhrZqfgkT .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fSqk1RuZhrZqfgkT .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fSqk1RuZhrZqfgkT .actorPopupMenu{position:absolute;}#mermaid-svg-fSqk1RuZhrZqfgkT .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-fSqk1RuZhrZqfgkT .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fSqk1RuZhrZqfgkT .actor-man circle,#mermaid-svg-fSqk1RuZhrZqfgkT line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-fSqk1RuZhrZqfgkT :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}alt[code == 0][网络异常或 code != 0]alt[校验失败][校验通过]用户点击提交validateForm()展示字段错误formatContactMessage(formData, source)text contentPOST JSON (timeout=10s){ code: 0, msg: "ok" }推送消息跳转 /contact-successerror/timeout显示提交失败用户

    5.4 流程图(成功/失败/重试/兜底)

    #mermaid-svg-b5JQF2Gx34KKrLcc{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-b5JQF2Gx34KKrLcc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-b5JQF2Gx34KKrLcc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-b5JQF2Gx34KKrLcc .error-icon{fill:#552222;}#mermaid-svg-b5JQF2Gx34KKrLcc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-b5JQF2Gx34KKrLcc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-b5JQF2Gx34KKrLcc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-b5JQF2Gx34KKrLcc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-b5JQF2Gx34KKrLcc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-b5JQF2Gx34KKrLcc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-b5JQF2Gx34KKrLcc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-b5JQF2Gx34KKrLcc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-b5JQF2Gx34KKrLcc .marker.cross{stroke:#333333;}#mermaid-svg-b5JQF2Gx34KKrLcc svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-b5JQF2Gx34KKrLcc p{margin:0;}#mermaid-svg-b5JQF2Gx34KKrLcc .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-b5JQF2Gx34KKrLcc .cluster-label text{fill:#333;}#mermaid-svg-b5JQF2Gx34KKrLcc .cluster-label span{color:#333;}#mermaid-svg-b5JQF2Gx34KKrLcc .cluster-label span p{background-color:transparent;}#mermaid-svg-b5JQF2Gx34KKrLcc .label text,#mermaid-svg-b5JQF2Gx34KKrLcc span{fill:#333;color:#333;}#mermaid-svg-b5JQF2Gx34KKrLcc .node rect,#mermaid-svg-b5JQF2Gx34KKrLcc .node circle,#mermaid-svg-b5JQF2Gx34KKrLcc .node ellipse,#mermaid-svg-b5JQF2Gx34KKrLcc .node polygon,#mermaid-svg-b5JQF2Gx34KKrLcc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-b5JQF2Gx34KKrLcc .rough-node .label text,#mermaid-svg-b5JQF2Gx34KKrLcc .node .label text,#mermaid-svg-b5JQF2Gx34KKrLcc .image-shape .label,#mermaid-svg-b5JQF2Gx34KKrLcc .icon-shape .label{text-anchor:middle;}#mermaid-svg-b5JQF2Gx34KKrLcc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-b5JQF2Gx34KKrLcc .rough-node .label,#mermaid-svg-b5JQF2Gx34KKrLcc .node .label,#mermaid-svg-b5JQF2Gx34KKrLcc .image-shape .label,#mermaid-svg-b5JQF2Gx34KKrLcc .icon-shape .label{text-align:center;}#mermaid-svg-b5JQF2Gx34KKrLcc .node.clickable{cursor:pointer;}#mermaid-svg-b5JQF2Gx34KKrLcc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-b5JQF2Gx34KKrLcc .arrowheadPath{fill:#333333;}#mermaid-svg-b5JQF2Gx34KKrLcc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-b5JQF2Gx34KKrLcc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-b5JQF2Gx34KKrLcc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b5JQF2Gx34KKrLcc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-b5JQF2Gx34KKrLcc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b5JQF2Gx34KKrLcc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-b5JQF2Gx34KKrLcc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-b5JQF2Gx34KKrLcc .cluster text{fill:#333;}#mermaid-svg-b5JQF2Gx34KKrLcc .cluster span{color:#333;}#mermaid-svg-b5JQF2Gx34KKrLcc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-b5JQF2Gx34KKrLcc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-b5JQF2Gx34KKrLcc rect.text{fill:none;stroke-width:0;}#mermaid-svg-b5JQF2Gx34KKrLcc .icon-shape,#mermaid-svg-b5JQF2Gx34KKrLcc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-b5JQF2Gx34KKrLcc .icon-shape p,#mermaid-svg-b5JQF2Gx34KKrLcc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-b5JQF2Gx34KKrLcc .icon-shape rect,#mermaid-svg-b5JQF2Gx34KKrLcc .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-b5JQF2Gx34KKrLcc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-b5JQF2Gx34KKrLcc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-b5JQF2Gx34KKrLcc :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    失败

    通过

    code=0

    异常/超时/code!=0

    是

    否

    用户点击提交

    前端校验

    展示字段错误并结束

    组装飞书文本消息

    axios POST 到 webhook

    跳转成功页

    展示失败提示

    用户是否重试提交

    人工兜底跟进线索


    6. 前端实现详解(ContactForm.vue)

    6.1 字段与校验规则

    当前表单字段:

    • name
    • email
    • phone
    • subject
    • message

    校验逻辑:

    • 所有字段必填(trim())
    • email 用正则校验格式
    • 任一失败即中断提交并显示本地化错误文案

    6.2 提交流程关键代码(节选)

    // ContactForm.vue (节选)
    // 1) 阻止默认提交 2) 前端校验 3) 调飞书 4) 成功跳转/失败提示
    async handleSubmit(event) {
    event.preventDefault()
    if (!this.validateForm()) return

    this.isSubmitting = true
    this.messageStatus = null

    try {
    await this.sendToFeishu()
    setTimeout(() => this.$router.push('/contact-success'), 1000)
    } catch (error) {
    console.error('Error sending message:', error) // 当前仅前端日志
    this.messageStatus = { type: 'error', text: this.getFormMessage('error') }
    this.isSubmitting = false
    }
    }

    6.3 请求发送关键代码(节选)

    // ContactForm.vue (节选)
    // 组装消息并直接 POST 到飞书群机器人 Webhook
    async sendToFeishu() {
    const webhookUrl = FEISHU_CONFIG.WEBHOOK_URL
    const sourceLabel = this.formCopy.source || 'Website'

    const messageContent = FeishuUtils.formatContactMessage(this.formData, sourceLabel)
    const message = FeishuUtils.buildMessage(messageContent)

    const response = await axios.post(webhookUrl, message, FEISHU_CONFIG.API_CONFIG)
    if (response.data.code !== 0) {
    throw new Error(`Feishu API error: ${response.data.msg}`)
    }
    return response.data
    }

    实现特点:

    • 有 loading 状态:isSubmitting
    • 有错误提示:messageStatus
    • 无后端参与,链路全在前端
    • 失败日志主要依赖浏览器控制台

    7. 飞书消息模块详解(feishu.js)

    7.1 模块职责

    lada/src/config/feishu.js 主要负责:

    • 使用 FEISHU_WEBHOOK_KEY 拼接群机器人 webhook URL
    • 提供消息模板函数(生成通知正文)
    • 把表单数据转为飞书 text 消息体

    7.2 核心代码(脱敏节选)

    // feishu.js (脱敏节选)
    export const FEISHU_CONFIG = {
    WEBHOOK_URL: 'https://example.com/open-apis/bot/v2/hook/<REDACTED>',
    API_CONFIG: {
    timeout: 10000,
    headers: { 'Content-Type': 'application/json' }
    }
    }

    export const FeishuUtils = {
    // 用模板 + 表单数据生成文本
    formatContactMessage(formData, source) {
    const template = '来源:{source} 姓名:{name} 邮箱:{email} 电话:{phone} 方向:{subject} 需求:{message}'
    return template
    .replace('{source}', source)
    .replace('{name}', formData.name)
    .replace('{email}', formData.email)
    .replace('{phone}', formData.phone)
    .replace('{subject}', formData.subject)
    .replace('{message}', formData.message)
    },

    // 构造飞书 text 消息格式
    buildMessage(content) {
    return {
    msg_type: 'text',
    content: { text: content }
    }
    }
    }

    7.3 消息结构说明

    字段说明
    msg_type 消息类型,当前为 text
    content.text 文本正文,来自模板替换
    {name} {email} {phone} {subject} {message} {source} {timestamp} 模板占位符

    模板定义位置:

    • lada/src/config/feishu.js(MESSAGE_TEMPLATE 与 formatContactMessage)

    8. 安全与脱敏设计(纯前端场景)

    8.1 纯前端直连的风险与现实边界

    由于没有后端,Webhook 相关配置无法做到服务端级别的隔离,因此要明确:

    • 前端方案更偏“快速接入”而非“高安全隔离”
    • 不适合高敏感、高对抗场景

    8.2 当前可执行的安全措施

  • 不把真实密钥提交仓库:项目已通过 .gitignore 忽略 src/config/apiKey.js。
  • 飞书群机器人开启安全策略(按飞书教程可配置项执行):
    • 关键词校验
    • IP 白名单(若网络出口固定时)
    • 签名校验(若业务允许在前端使用该模式)
  • 前端最小化暴露:仅保留必要字段,不在日志打印完整用户隐私。
  • 脱敏规范:文档、截图、示例代码全部使用 <REDACTED> 与假数据。
  • 8.3 必须强调

    如果代码里出现真实值,请务必改为部署注入,不要提交到仓库。


    9. 部署与配置指南(纯前端)

    9.1 飞书侧配置步骤

  • 企业管理员为开发人员开通飞书开发者权限。
  • 在飞书开放平台创建应用。
  • 给应用添加“机器人”能力。
  • 按飞书官方教程在目标群创建群机器人并拿到 webhook 链接。
  • 9.2 项目侧配置

    本项目使用 src/config/apiKey.js 存放 key(本地文件,不入库):

    // src/config/apiKey.js(示例,已脱敏)
    export const API_KEY = {
    AMAP_KEY: '<REDACTED>',
    FEISHU_WEBHOOK_KEY: '<REDACTED>'
    }

    说明:FEISHU_WEBHOOK_KEY 对应 webhook 链接里 …/hook/ 后面的部分。


    10. 联调与验证

    10.1 本地验证步骤

  • 启动前端:npm run serve
  • 配置 src/config/apiKey.js(使用测试环境密钥)
  • 使用假数据提交表单:张三 / test@example.com / 138****0000
  • 浏览器 Network 检查:请求 URL、HTTP 状态、响应体
  • 群内确认是否收到通知消息
  • 10.2 请求消息示例(发送到飞书)

    {
    "msg_type": "text",
    "content": {
    "text": "🎯 新的咨询表单提交\\n\\n🌐 来源: 官网\\n👤 联系人: 张三\\n📧 电子邮箱: test@example.com\\n📞 联系电话: 138****0000\\n🎯 咨询方向: 产品咨询\\n\\n📋 详细需求:\\n希望了解 AI 解决方案\\n\\n⏰ 提交时间: 2026/02/12 10:00:00"
    }
    }

    10.3 验证通过标准

    • 正常提交:页面跳转成功页,群里收到消息
    • 校验失败:前端提示明确,不发请求
    • 发送失败:前端提示失败,开发者可在 Network/Console 中定位

    11. 常见问题排查

    问题现象常见原因处理建议
    400 Bad Request 消息结构错误、缺字段、JSON 格式不对 校验 msg_type 与 content.text,打印请求体
    401/403 Webhook 无效、群机器人权限或安全策略不匹配 检查 webhook 是否完整、机器人是否可用
    签名相关错误 签名配置开启但请求未按规则生成 对齐飞书机器人签名配置与实现
    未收到群消息 机器人未添加到目标群、群权限限制 确认机器人在群内且可发言
    提交一直转圈 异常分支未正确回收状态 确保 catch 分支恢复 isSubmitting
    模板占位符未替换 模板字符串或占位符拼写错误 对照 formatContactMessage 的占位符命名校验

    12. 可扩展优化

  • 消息卡片化:从 text 升级为 interactive card,字段更结构化。
  • 客户端重试:增加指数退避重试与“稍后重试”按钮。
  • 发送前防刷:接入前端验证码、人机校验。
  • 埋点监控:记录提交成功率、失败码分布、重试率。
  • 多群路由策略:按业务线把不同咨询方向发送到不同飞书群机器人。
  • 表单字段扩展:增加公司、预算、期望上线时间等业务字段。
  • 赞(0)
    未经允许不得转载:171主机测评 » 自动化“联系我们”:纯前端接入飞书群机器人消息通知实践
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址