自动化“联系我们”:纯前端接入飞书群机器人消息通知实践
1. 背景与目标
在官网“联系我们”页面里,用户线索如果不能第一时间触达业务群,常见问题就是响应慢、跟进晚、丢线索。
本文聚焦一个纯前端项目里的可落地方案:
- 用户提交联系表单
- 前端完成校验与消息组装
- 直接调用飞书群机器人 Webhook
- 群内实时收到线索通知
本文不涉及后端接口实现。
2. 功能效果展示


用户体验路径:
3. 技术选型与方案说明(纯前端)
3.1 本文方案
当前项目采用的是:前端直连飞书群机器人 Webhook。
优点:
- 接入成本低
- 链路短,联调快
- 适合官网线索通知这类轻量场景
边界:
- Webhook 相关配置在前端侧存在暴露风险
- 风控能力有限(需依赖飞书机器人安全设置)
3.2 接入前置条件(你必须先完成)
本项目中,代码保存的是 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
- 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 当前可执行的安全措施
- 关键词校验
- IP 白名单(若网络出口固定时)
- 签名校验(若业务允许在前端使用该模式)
8.3 必须强调
如果代码里出现真实值,请务必改为部署注入,不要提交到仓库。
9. 部署与配置指南(纯前端)
9.1 飞书侧配置步骤
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 本地验证步骤
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 的占位符命名校验 |



