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

原文: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实现这个需求再合适不过了。

1.实现Websocket服务

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

javascript 复制代码
export const corpClients = new Map();

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

javascript 复制代码
const wss = new WebSocketServer({host: '0.0.0.0', port: CONFIG.ports.ws});
  Logger.info(`WebSocket服务器运行在 ws://localhost:${CONFIG.ports.ws}`);

  wss.on('connection', (ws, req) => {

    let ip = req.headers['x-real-ip']
        || req.headers['x-forwarded-for']?.split(',')[0].trim()
        || req.socket.remoteAddress;

    if (ip.substr(0, 7) === '::ffff:') ip = ip.substr(7);

    Logger.debug(`新的WebSocket连接,${JSON.stringify({remoteAddress: ip, url: req.url})}`);
    Logger.info(`当前连接数: ${getConnectionCount()}`);

    ws.on('close', () => {
      if (ws.corp && ws.phone) {
        const companyMap = corpClients.get(ws.corp);
        if (companyMap) {
          companyMap.delete(ws.phone);
          if (companyMap.size === 0) corpClients.delete(ws.corp);
        }
        Logger.debug('WebSocket连接关闭', {phone: ws.phone, corp: ws.corp, remainingClients: getConnectionCount()});
      }
    });

    ws.on('error', (error) => {
      Logger.error('WebSocket连接错误', error);
      if (ws.corp && ws.phone) {
        const companyMap = corpClients.get(ws.corp);
        if (companyMap) {
          companyMap.delete(ws.phone);
          if (companyMap.size === 0) corpClients.delete(ws.corp);
        }
      }
    });
  });

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

javascript 复制代码
ws.on('message', (body) => {

      const data = body.toString();
      Logger.info(`收到消息:${data},${JSON.stringify({remoteAddress: ip})}`);

      /* 维持心跳 */
      if (data === 'ping') {
        ws.send("1");
        return;
      }

      try {
        const msg = JSON.parse(data);
        if (!ws.phone && msg.type === 'login') {
          if (msg?.name && msg?.corp) {
            const phone = msg.name;
            const corp = String(msg.corp);
            let companyMap = corpClients.get(corp);
            if (!companyMap) {
              companyMap = new Map();
              corpClients.set(corp, companyMap);
            }
            const existingClient = companyMap.get(phone);
            if (existingClient) {
              Logger.info(`禁止重复登录,通知${phone}自动退出!`);
              existingClient.send(JSON.stringify({type: 'quit', name: phone}));
              companyMap.delete(phone);
            }
            Logger.info(`新消息:${phone}上线`);
            ws.phone = phone;
            ws.corp = corp;
            companyMap.set(phone, ws);
          }
        }
      } catch (error) {
        Logger.error('处理WebSocket消息失败', error);
      }
    });

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

2.实现http接口

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

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

javascript 复制代码
const app = new Hono();

const param = async (c) => {
    let data = {};
    const queryParams = c.req.query();
    if (Object.keys(queryParams).length > 0) data = {...data, ...queryParams};
    if (c.req.method === 'POST') {
        const contentType = c.req.header('content-type') || '';
        try {
            if (contentType.includes('application/json')) {
                const jsonData = await c.req.json();
                data = {...data, ...jsonData};
            } else if (contentType.includes('multipart/form-data') || contentType.includes('application/x-www-form-urlencoded')) {
                const formData = await c.req.parseBody();
                data = {...data, ...formData};
            }
        } catch (parseError) {
            Logger.warn('解析请求数据失败', parseError);
        }
    }
    return data;
};

app.all('/send', async (c) => {
    try {
        const data = await param(c);
        Logger.debug('收到请求', data);
        if (!!data.phone && !!data.name && !!data.type && !!data.corp) {
            const companyMap = corpClients.get(String(data.corp));
            const client = companyMap ? companyMap.get(data.name) : null;
            if (!client) return c.json({code: 0, message: '你的APP账号没有上线!'}, 200);
            client.send(JSON.stringify(data)); /* 操作 */
            return c.json({code: 1, message: 'ok'});
        } else {
            return c.json({code: 0, message: 'fail'});
        }
    } catch (error) {
        Logger.error('处理webhook请求失败', error);
        return c.json({code: 0, message: 'error'}, 200);
    }
});

serve({fetch: app.fetch, port: CONFIG.ports.http}, (info) => {
    Logger.info(`HTTP服务器运行在 http://localhost:${info.port}`);
    Logger.info(`调试模式: ${CONFIG.debug ? '开启' : '关闭'}`);
});

return app;

3.APP端初始化Ws链接

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

javascript 复制代码
/* 链接websocket */
const {status, data, close, open, send, ws} = useWebSocket(api.ws, {
    onConnected(ws) {
        ws.send(JSON.stringify({
            type: 'login',
            corp: user.corp.id,
            name: user.username
        }));
    },
    onMessage(ws, event) {

        /* 接受数据 */
        if (event.data === "1") return;
        const data = JSON.parse(event.data);

        /* 处理,后台应用弹出 */
        moveTop();

        /* 登录同一个账号,退出登录 */
        if (data.type === "quit") {
            quit________system();
            return load.info('您的账号在别处登录');
        } else if (data.type === "call") {
            call({phone: data.phone});
        }

    },
    heartbeat: {
        interval: 5000
    },
    immediate: true,
    autoClose: false,
    autoReconnect: true
});

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

javascript 复制代码
/* 初始化监听:监控用户 token 登录与退出,自动建立/关闭 ws */
export default function initWsWatch() {
    /* 用户信息 */
    const user = _user();
    /* 监听 token 变化:有值则连接,无值则关闭并重置 */
    watch(() => user.token, (token) => {
        if (token) {
            connectWs();
        } else {
            disconnectWs(true);
        }
    }, {immediate: true});
}
相关推荐
地狱恶犬萨煤耶1 小时前
JavaScript-实现函数方法-改变this指向call apply bind
javascript
地狱恶犬萨煤耶1 小时前
JavaScript-小游戏-单词消消乐
javascript
tyro曹仓舒2 小时前
干了10年前端,才学会使用IntersectionObserver
前端·javascript
mine_mine3 小时前
油猴脚本拦截fetch和xhr请求,实现修改服务端接口功能
javascript
一 乐3 小时前
考公|考务考试|基于SprinBoot+vue的考公在线考试系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot·课程设计
林太白4 小时前
跟着TRAE SOLO全链路看看项目部署服务器全流程吧
前端·javascript·后端
特级业务专家4 小时前
把 16MB 中文字体压到 400KB:我写了一个 Vite 字体子集插件
javascript·vue.js·vite
先生沉默先4 小时前
NodeJs 学习日志(8):雪花算法生成唯一 ID
javascript·学习·node.js
起这个名字4 小时前
Webpack——插件实现的理解
前端·javascript·node.js