基于UniappX开发电销APP,实现CRM后台控制APP自动拨号

作者:爱心发电丶日期:2025/11/19

原文:nicen.cn/8532.html

在上一篇文章中(juejin.cn/post/757352…),已经实现了电销APP的基础功能:通时通次记录、通话录音上传。 已经能在工作中进行应用了,但是离成熟的电销APP还是差了不少,还得继续开发。

电销APP大都还有一个与之对应的CRM系统,所以另一个常见的需求,就是通过CRM后台直接控制APP拨号。

相关代码和电销APP已经开源:github.com/friend-nice…

开发思路

常规需求用常规的办法:在保证消息收发高效实时的前提下,后端实现一个Websocket服务用于和APP用户端实时通信,再通过http提供接口给CRM后台调用,触发APP端拨号。从技术栈匹配度来看,用Node实现这个需求再合适不过了。

基于UniappX开发电销APP,实现CRM后台控制APP自动拨号

1.实现Websocket服务

考虑到我的需求面向的是Saas多公司的系统,所以先实现一个方便存取ws链接的连接池对象,嵌套的两层Map对象,第一层是区分哪个公司,然后是哪个用户:

1export const corpClients = new Map();
2

通过ws来实现Websocket服务,简单的几行代码,再加上自己的业务逻辑。

1const wss = new WebSocketServer({host: '0.0.0.0', port: CONFIG.ports.ws});
2  Logger.info(`WebSocket服务器运行在 ws://localhost:${CONFIG.ports.ws}`);
3
4  wss.on('connection', (ws, req) => {
5
6    let ip = req.headers['x-real-ip']
7        || req.headers['x-forwarded-for']?.split(',')[0].trim()
8        || req.socket.remoteAddress;
9
10    if (ip.substr(0, 7) === '::ffff:') ip = ip.substr(7);
11
12    Logger.debug(`新的WebSocket连接,${JSON.stringify({remoteAddress: ip, url: req.url})}`);
13    Logger.info(`当前连接数: ${getConnectionCount()}`);
14
15    ws.on('close', () => {
16      if (ws.corp && ws.phone) {
17        const companyMap = corpClients.get(ws.corp);
18        if (companyMap) {
19          companyMap.delete(ws.phone);
20          if (companyMap.size === 0) corpClients.delete(ws.corp);
21        }
22        Logger.debug('WebSocket连接关闭', {phone: ws.phone, corp: ws.corp, remainingClients: getConnectionCount()});
23      }
24    });
25
26    ws.on('error', (error) => {
27      Logger.error('WebSocket连接错误', error);
28      if (ws.corp && ws.phone) {
29        const companyMap = corpClients.get(ws.corp);
30        if (companyMap) {
31          companyMap.delete(ws.phone);
32          if (companyMap.size === 0) corpClients.delete(ws.corp);
33        }
34      }
35    });
36  });
37

考虑到用户连接之后,得区分这个连接是哪个用户,所以还得有一个类似登录的过程,登录之后的链接才会保存到连接池。

1ws.on('message', (body) => {
2
3      const data = body.toString();
4      Logger.info([`收到消息:${data}${JSON.stringify({remoteAddress: ip})}`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.data.md));
5
6      /* 维持心跳 */
7      if (data === 'ping') {
8        ws.send("1");
9        return;
10      }
11
12      try {
13        const msg = JSON.parse(data);
14        if (!ws.phone && msg.type === 'login') {
15          if (msg?.name && msg?.corp) {
16            const phone = msg.name;
17            const corp = String(msg.corp);
18            let companyMap = corpClients.get(corp);
19            if (!companyMap) {
20              companyMap = new Map();
21              corpClients.set(corp, companyMap);
22            }
23            const existingClient = companyMap.get(phone);
24            if (existingClient) {
25              Logger.info(`禁止重复登录,通知${phone}自动退出!`);
26              existingClient.send(JSON.stringify({type: 'quit', name: phone}));
27              companyMap.delete(phone);
28            }
29            Logger.info(`新消息:${phone}上线`);
30            ws.phone = phone;
31            ws.corp = corp;
32            companyMap.set(phone, ws);
33          }
34        }
35      } catch (error) {
36        Logger.error('处理WebSocket消息失败', error);
37      }
38    });
39

提示:考虑到安全性的问题,可以继续完善做进一步的认证

2.实现http接口

CRM后台唤起拨号的这个操作是不定时不定量的,所以不需要考虑到实时性的问题,所以通过提供http接口来实现是完全够用的。

通过Hono来快速实现接口,然后调用上面保存的连接池来向APP发送实时消息:

1const app = new Hono();
2
3const param = async (c) => {
4    let data = {};
5    const queryParams = c.req.query();
6    if (Object.keys(queryParams).length > 0) data = {...data, ...queryParams};
7    if (c.req.method === 'POST') {
8        const contentType = c.req.header('content-type') || '';
9        try {
10            if (contentType.includes('application/json')) {
11                const jsonData = await c.req.json();
12                data = {...data, ...jsonData};
13            } else if (contentType.includes('multipart/form-data') || contentType.includes('application/x-www-form-urlencoded')) {
14                const formData = await c.req.parseBody();
15                data = {...data, ...formData};
16            }
17        } catch (parseError) {
18            Logger.warn('解析请求数据失败', parseError);
19        }
20    }
21    return data;
22};
23
24app.all('/send', async (c) => {
25    try {
26        const data = await param(c);
27        Logger.debug('收到请求', data);
28        if (!!data.phone && !!data.name && !!data.type && !!data.corp) {
29            const companyMap = corpClients.get(String(data.corp));
30            const client = companyMap ? companyMap.get(data.name) : null;
31            if (!client) return c.json({code: 0, message: '你的APP账号没有上线!'}, 200);
32            client.send(JSON.stringify(data)); /* 操作 */
33            return c.json({code: 1, message: 'ok'});
34        } else {
35            return c.json({code: 0, message: 'fail'});
36        }
37    } catch (error) {
38        Logger.error('处理webhook请求失败', error);
39        return c.json({code: 0, message: 'error'}, 200);
40    }
41});
42
43serve({fetch: app.fetch, port: CONFIG.ports.http}, (info) => {
44    Logger.info(`HTTP服务器运行在 http://localhost:${info.port}`);
45    Logger.info(`调试模式: ${CONFIG.debug ? '开启' : '关闭'}`);
46});
47
48return app;
49

3.APP端初始化Ws链接

App端运行的稳定性没法保证,所以需要考虑各种极端条件,保证Ws连接的稳定性。下面是通过Vueuse来实现的Ws,Vuesuse已经封装好了自动重连、心跳包等逻辑:

1/* 链接websocket */
2const {status, data, close, open, send, ws} = useWebSocket(api.ws, {
3    onConnected(ws) {
4        ws.send(JSON.stringify({
5            type: 'login',
6            corp: user.corp.id,
7            name: user.username
8        }));
9    },
10    onMessage(ws, event) {
11
12        /* 接受数据 */
13        if (event.data === "1") return;
14        const data = JSON.parse(event.data);
15
16        /* 处理,后台应用弹出 */
17        moveTop();
18
19        /* 登录同一个账号,退出登录 */
20        if (data.type === "quit") {
21            quit________system();
22            return load.info('您的账号在别处登录');
23        } else if (data.type === "call") {
24            call({phone: data.phone});
25        }
26
27    },
28    heartbeat: {
29        interval: 5000
30    },
31    immediate: true,
32    autoClose: false,
33    autoReconnect: true
34});
35

继续封装,用户登录的时候初始化链接,用户退出的时候自动断开:

1/* 初始化监听:监控用户 token 登录与退出,自动建立/关闭 ws */
2export default function initWsWatch() {
3    /* 用户信息 */
4    const user = _user();
5    /* 监听 token 变化:有值则连接,无值则关闭并重置 */
6    watch(() => user.token, (token) => {
7        if (token) {
8            connectWs();
9        } else {
10            disconnectWs(true);
11        }
12    }, {immediate: true});
13}
14

基于UniappX开发电销APP,实现CRM后台控制APP自动拨号》 是转载文章,点击查看原文


相关推荐


Swift 6 迁移常见 crash: _dispatch_assert_queue_fail
RickeyBoy2025/11/17

我的 Github:github.com/RickeyBoy/R… 大量 iOS 内容欢迎大家关注~ 最近在将公司项目迁移到 Swift 6 的过程中,解决了好几个相似的 crash。关键字如下 _dispatch_assert_queue_fail "%sBlock was %sexpected to execute on queue [%s (%p)] Task 208: EXC_BREAKPOINT (code=1, subcode=0x103


VSCode debugger 调试指南
清沫2025/11/16

在以前的文章 深入前端调试原理,我们主要从原理的角度来看了如何调试。本篇文章则是从实践角度出发,看看如何在 vscode 中配置和使用各种调试功能。 本文涉及到代码均在仓库 vscode-debugger-dojo,全面的 VSCode Debugger 调试示例项目,涵盖了各种常见场景的调试配置。 VSCode Debugger 原理 在 VSCode 的项目中 .vscode/launch.json 中加入如下的配置即可调试: SCode 并不是 JS 语言的专属编辑器,它可以用于多


Bash 的 base64 命令
hubenchang05152025/11/15

#Bash 的 base64 命令 base64 [OPTION]... [FILE]... 功能 进行 BASE64 编码或解码。 类型 可执行文件(/usr/bin/base64),属于 coreutils。 参数 OPTION 选项: -d, --decode - 解码;不带此选项则为编码 -i, --ignore-garbage - 解码时忽略无效字符 -w, --wrap=COLS - 编码输出时一行的字符数;默认为 76,设为 0 则不换行 --help - 显示帮助 --ve


Vue3实现拖拽排序
用户9714171814272025/11/14

Vue3 + Element Plus + SortableJS 实现表格拖拽排序功能 📋 目录 功能概述 技术栈 实现思路 代码实现 核心要点 常见问题 总结 功能概述 在管理后台系统中,表格数据的排序功能是一个常见的需求。本文介绍如何使用 Vue3、Element Plus 和 SortableJS 实现一个完整的表格拖拽排序功能,支持: ✅ 通过拖拽图标对表格行进行排序 ✅ 实时更新数据顺序 ✅ 支持数据过滤后的排序 ✅ 切换标签页时自动初始化 ✅ 优雅的动画效果 先看实现效果:


Python 的内置函数 id
IMPYLH2025/11/13

Python 内建函数列表 > Python 的内置函数 id Python 的内置函数 id() 是一个非常有用的工具函数,它返回一个对象的"身份标识",这个标识实际上是该对象在内存中的地址(以整数形式表示)。以下是关于 id() 函数的详细说明: 基本用法 x = 42 print(id(x)) # 输出一个整数,代表变量x所引用的对象的内存地址 特性说明 每个对象在内存中都有唯一的id这个id在对象的生命周期内保持不变不同对象可能有相同的id(如果前一个对象已被销毁)


FastAPI × SQLAlchemy 2.0 Async:从“能跑”到“可压测”的完整工程实践
Java私教2025/11/11

一句话总结 用 SQLAlchemy 2.0 AsyncIO 模式,把 FastAPI 的并发优势兑现成 真正的数据库吞吐;再叠上连接池、事务、迁移、测试四件套,直接上线不踩坑。 1. 为什么要“异步 ORM”? 场景同步 SQLAlchemy异步 SQLAlchemy100 个并发上传开 100 线程 → 100 个连接 → DB 被打爆单线程 20 连接即可跑满 CPU请求等待 I/O线程上下文切换 8 ms协程切换 0.3 ms代码风格


删一个却少俩:Antd Tag 多节点同时消失的原因
顺凡2025/11/9

删一个却少俩:Antd Tag 多节点同时消失的原因 需求 一个表单的小需求,能填写多个福利,最多十个,福利名称允许重复,和官方的动态添加和删除示例交互一模一样,只是官方示例不支持 tag 内容重复,使用的 tag 内容作为 key 我复制丢给 AI,下掉去重,限制个数,好!满足需求了,key 值怎么办不能用重复的,拼个索引吧,最后主要代码如下, 反问一下:你觉得这会有什么问题,能达到删一个少俩的效果吗🤔??? 问题 大家应该都知道用 index 作为 key,会有一些问题,对于我这个需


程序员副业 | 2025年10月复盘
嘟嘟MD2025/11/7

本文首发于公众号:嘟爷创业日记 。 我已经坚持日更600天+,欢迎过来追剧~ 大家好,我是嘟嘟MD,一个10年程序员,现在离职创业,有700天了,我每个月都会写一篇总结复盘,让大家可以近距离看看一个离职程序员都在干什么,今天这篇是九月份的总结,大概2000字,略长,有空的可以翻翻,希望对大家有一丢丢的借鉴作用! 一、月度大事 10月结束了,一直拖到现在才有空汇总下10月份的进度,整体来说对外的合作少了,组织内的事情多了。 1:公众号运营+B站视频运营 公众号和B站视频运营还是我的最高优先级


在 Vue3 项目中使用 el-tree
代码工人笔记2025/11/3

在 Vue3 项目中使用 el-tree 文章目录 一、基础用法1. 引入组件 二、常用功能与配置1. 节点选择(复选框 / 单选)2. 展开 / 折叠控制3. 自定义节点内容4. 搜索过滤节点5. 获取选中节点 三、动态加载节点 一、基础用法 1. 引入组件 <template> <el-tree :data="treeData" :props="defaultProps" @node-click="handleNode


SQL之表的查改(上)
啊吧怪不啊吧2025/10/31

目录 1. Retrieve 1.2 Select 1.2.1全列插入 1.2.2 use 1.2.3 指定列查询 1.2.4 select+固定值 1.2.5 列值修改查询 1.2.6 别名 1.2.7 查询结果去重 在前面的文章中我们链接了表的增删操作,今天我们来聊一下表的查找与修改操作。 1. Retrieve 首先我们要了解到Retrieve不是某一个具体的指令,它描述的是 “从数据库表中获取数据” 这一行为本身,而实现这种行为的具体技

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0