SpringBoot Vue 企业微信多账号托管|扫码登录 代理 IP 消息回调

企业微信多账号托管别靠人盯着扫:Spring Boot + Vue 实现扫码登录、代理城市与回调自动登记

SEO 摘要:本文分享企业微信多账号托管的完整落地实践,基于 Spring Boot + Vue 实现扫码登录、代理城市配置与回调自动登记。内容覆盖产品入口设计、技术架构、上号链路(init → 代理 → 扫码 → 入库 → 登记回调)、在线状态同步与授权槽位管理,并给出真实源码与现网边界说明,适合私域运营、后端开发与架构师参考。


目录

  • [1. 产品里三个入口,别搞混](#1. 产品里三个入口,别搞混)
  • [2. 技术架构:Vue 只负责扫码,在线和回调在 Spring Boot](#2. 技术架构:Vue 只负责扫码,在线和回调在 Spring Boot)
  • [3. 上号链路:init → 代理 → 扫码 → 入库 → 登记回调](#3. 上号链路:init → 代理 → 扫码 → 入库 → 登记回调)
    • [3.1 标准准备:先拿 uuid,再决定要不要代理](#3.1 标准准备:先拿 uuid,再决定要不要代理)
    • [3.2 选「本机」等于拆掉代理](#3.2 选「本机」等于拆掉代理)
    • [3.3 扫完码:先拉资料,再写入库](#3.3 扫完码:先拉资料,再写入库)
    • [3.4 重新登录不是再扫一遍那么简单](#3.4 重新登录不是再扫一遍那么简单)
    • [3.5 回调地址必须是网关能 POST 到的公网](#3.5 回调地址必须是网关能 POST 到的公网)
  • [4. 在线状态:每分钟对一次,聊天回调负责纠偏](#4. 在线状态:每分钟对一次,聊天回调负责纠偏)
  • [5. 现网边界](#5. 现网边界)
  • [6. 落地清单](#6. 落地清单)
  • [7. 总结](#7. 总结)

技术栈说明

本教程全文使用的底层API调用地址:https://wechatapi.apifox.cn/

代码调用示例参考官网:https://www.jikehudong.com/

开发语言:c + java

开发框架:Spring Boot + Vue


私域多号运营的真实痛点不是「没有企微」,而是这三件事叠在一起:

  1. 号在手机里,人在电脑前:十几个号轮流打开 App 扫码,掉线了还得客服自己发现。
  2. 城市和出口对不上:同一出口 IP 挂一堆号,比真人还整齐,风控一眼能看出来。
  3. 会话散落各处:A 号的客户在这台电脑回,B 号的客户在那台手机回,交接全靠截图。

很多团队的做法是:每台电脑开一个企微客户端,人盯着二维码。这解决不了授权槽位、登录城市、消息回调、在线状态、工作台聚合这些问题。

我们在 极客聚合(企业微信聚合平台) 里把这条链路做成了现网功能:后台扫码上号,登录城市走系统里配好的代理,回调地址由服务端自动登记,在线状态每分钟对一次网关。下面全部是真实菜单名和源码,没有「无限并发几千号同时在线扫」这种会把号送走的能力。


1. 产品里三个入口,别搞混

多账号托管不是「一个登录按钮」,而是三层配置:

入口 菜单路径 干什么
上号与库存 企微账号管理 → 账号管理 统计卡、账号列表、登录 / 退出 / 二次验证 / 日志
按号配能力 企微账号管理 → 账号设置 这个号开不开关键字、口令入群、自动拉人、AI
自动化时段 企微账号管理 → 工作时间 未设置或已禁用的号,不受工作时间限制

旁边还有两个底座,不在这个菜单里,但不上号就跑不起来:

入口 菜单路径 干什么
登录城市 系统设置 → 配置代理 城市名会出现在「登录企业微信」的下拉框
授权上限 系统设置 → 成员管理 → 托管账号上限 非 admin 按「上限 − 在线数」扣槽位
会话聚合 客服工作台 侧边栏按钮打开独立窗口,多号切会话

账号管理页顶部四张卡:已购买授权、已登录账号、已到期授权、离线账号。admin 的剩余可用授权显示 「无限」 ,普通成员才按 account_limit 扣。

点「登录企业微信」是两步向导:初始化 → 扫码登录 。页面写死了两件事:消息回调地址由后台配置;登录城市来自 系统设置 → 配置代理。首次登录后约 30 分钟内可能二次验证,建议尽量等满这段再密集上号。

号上完只是「能在线」。真正按号配自动化,要切到 账号设置,先点账号卡片:

进账号后是四张能力卡,和 AI 客服、加好友不是一个开关包打天下------每个号自己勾:

工作时间是第三层闸。页面提示很直白:未设置或已禁用规则的账号,不受工作时间限制。 没配规则不等于「全天停机」,而是自动化不受这段约束。

登录城市不是手填 IP。运营在 配置代理 里按服务商「固定 IP」列表录入:省市 → 创建机器人时的展示名;IP / 端口 / 账号 / 密码 → 下发到网关 /wxwork/setProxy。代理类型只支持 http / socks5 。选「本机」等于 127.0.0.1:1081 http,等同移除代理。

号都在线之后,客服不用在十几台电脑之间切。侧边栏「客服工作台」打开独立窗口:左侧切托管号,中间会话,右侧话术库。号离线时页面会说实话------只展示本地缓存或已落库消息生成的会话,不会假装还在实时收消息。


2. 技术架构:Vue 只负责扫码,在线和回调在 Spring Boot

整体不是「前端拿着二维码直连网关」。Vue 负责选城市、展示二维码、点登录 / 退出。真正 initsetProxySetCallbackUrlGetRunClientByUuid,全在 Spring Boot。在线状态不是前端轮询硬撑,而是定时任务写回 MySQL,各业务模块只读这张表。
#mermaid-svg-C1kz1LPxQ1ko2SfJ{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-C1kz1LPxQ1ko2SfJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .error-icon{fill:#552222;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .marker.cross{stroke:#333333;}#mermaid-svg-C1kz1LPxQ1ko2SfJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-C1kz1LPxQ1ko2SfJ p{margin:0;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .cluster-label text{fill:#333;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .cluster-label span{color:#333;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .cluster-label span p{background-color:transparent;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .label text,#mermaid-svg-C1kz1LPxQ1ko2SfJ span{fill:#333;color:#333;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .node rect,#mermaid-svg-C1kz1LPxQ1ko2SfJ .node circle,#mermaid-svg-C1kz1LPxQ1ko2SfJ .node ellipse,#mermaid-svg-C1kz1LPxQ1ko2SfJ .node polygon,#mermaid-svg-C1kz1LPxQ1ko2SfJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .rough-node .label text,#mermaid-svg-C1kz1LPxQ1ko2SfJ .node .label text,#mermaid-svg-C1kz1LPxQ1ko2SfJ .image-shape .label,#mermaid-svg-C1kz1LPxQ1ko2SfJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .rough-node .label,#mermaid-svg-C1kz1LPxQ1ko2SfJ .node .label,#mermaid-svg-C1kz1LPxQ1ko2SfJ .image-shape .label,#mermaid-svg-C1kz1LPxQ1ko2SfJ .icon-shape .label{text-align:center;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .node.clickable{cursor:pointer;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .arrowheadPath{fill:#333333;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-C1kz1LPxQ1ko2SfJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C1kz1LPxQ1ko2SfJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-C1kz1LPxQ1ko2SfJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .cluster text{fill:#333;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .cluster span{color:#333;}#mermaid-svg-C1kz1LPxQ1ko2SfJ 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-C1kz1LPxQ1ko2SfJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-C1kz1LPxQ1ko2SfJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .icon-shape,#mermaid-svg-C1kz1LPxQ1ko2SfJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .icon-shape p,#mermaid-svg-C1kz1LPxQ1ko2SfJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .icon-shape .label rect,#mermaid-svg-C1kz1LPxQ1ko2SfJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-C1kz1LPxQ1ko2SfJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-C1kz1LPxQ1ko2SfJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-C1kz1LPxQ1ko2SfJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 存储
企微网关
Spring Boot
Vue 管理端
企微账号管理 / 账号列表
登录企业微信 / 选登录城市
系统设置 / 配置代理
客服工作台 / 多号会话
SysWxworkController

init / save / setCallback
prepareWxworkLoginInstance

init → setProxy
WxworkCallbackBindService
在线同步 GetRunClientByUuid

并行最多 3
授权槽位

account_limit − 在线数
init
GetLoginQrcode
automaticLogin
SetCallbackUrl
GetRunClientByUuid
MySQL sys_wxwork_robot
Redis 会话消息

关键约束写在架构图脚注里,也写进代码:

  • 登录城市来自配置代理,不是登录弹窗里手填 IP。
  • 二次验证:接口 -11025,或回调 type=100012(号会被标离线)。
  • 号离线后释放可登录数;槽位按 在线数 扣,不是按列表总行数扣。

3. 上号链路:init → 代理 → 扫码 → 入库 → 登记回调

3.1 标准准备:先拿 uuid,再决定要不要代理

前端 prepareWxworkLoginInstance 把三步收成一次调用。initWaitMs 默认 2 秒,选了城市代理则先 setProxy 再等 6 秒,给网关把出口切过去:

javascript 复制代码
export async function prepareWxworkLoginInstance(params = {}, options = {}) {
  const initRes = await initWxworkRobot({ vid }, options)
  const uuid = data.uuid || data.UUID
  if (proxyId > 0) {
    await applyWxworkProxy({ uuid, proxyId }, options)
    await sleep(params.proxyWaitMs != null ? params.proxyWaitMs : 6000)
  } else {
    await sleep(params.initWaitMs != null ? params.initWaitMs : 2000)
  }
  return { ...data, uuid: String(uuid) }
}

后端 POST /wxwork/init 固定 deverType=ipad。uuid 一回来就 bindQuietly 登记回调,不等扫码成功------这样扫码过程中已经能接到网关事件。

3.2 选「本机」等于拆掉代理

proxyId ≤ 0 时服务端不会省略 setProxy,而是显式打本机:

java 复制代码
if (proxyId <= 0) {
    req.put("ip", "127.0.0.1");
    req.put("port", 1081);
    req.put("proxyType", "http");
}

城市代理必须带齐 IP / 端口,类型只能是 http 或 socks5,否则网关会直接报 101211。代理表里的「备注」只做本系统备忘,不会 塞进 setProxy

3.3 扫完码:先拉资料,再写入库

POST /wxwork/save 不是把前端填的昵称当真理。在线时会带重试地调 GetRunClientByUuid,从 user_info.object 抽昵称、头像、企微号再入库。离线保存会跳过详情,避免把一个已经掉线的实例写成「在线且资料齐全」。

保存成功且不是离线,会再 bindQuietly 一次------init 时登记过,这里补一次,防止中途 uuid 变了。

3.4 重新登录不是再扫一遍那么简单

列表上的「登录」走 performLogin:有 vid 就 init + setProxy,必要时 automaticLogin,最后按数据库 id save。已经在线的号按钮是灰的,文案是「机器人已在线,无需重复登录」。

失败要按错误码分流,不能一律弹「登录失败」:

现象 处理
-11025 或回调 100012 打开二次验证二维码
SecondaryValidation 返回 -12007(大约超过 2 分钟) 自动改走常规扫码
automaticLogin 返回 -2007 自动登录凭证失效,同样改走常规扫码
「实例不存在 / uuid 失效」且没有 vid 把该号标离线,避免假在线占槽位

手机操作顺序页面也写死了:先点「是本人使用,去扫码验证」→ 扫网页二维码 → 再点「确定是本人使用」。顺序反了,二次验证会空转。

3.5 回调地址必须是网关能 POST 到的公网

java 复制代码
String bindUrl = wxworkCallbackUrl
        + (wxworkCallbackUrl.contains("?") ? "&" : "?")
        + "uuid=" + uuid;
req.put("uuid", uuid);
req.put("url", bindUrl);
// POST {wxwork.apiUrl}/wxwork/SetCallbackUrl

wxwork.callbackUrl 配成 127.0.0.1 / localhost 时,日志会警告:第三方访问不到。网关返回 404,提示隧道没开或域名失效;405 则是隧道 / Nginx 没把 POST 转到 /wxwork/callback。本机开发可以把地址配上,但不要指望局域网 IP 能接到云上的网关回调。


4. 在线状态:每分钟对一次,聊天回调负责纠偏

各业务(加好友分配、工作台轮询、群发选号)都只认库里的 online_status。真相来源是网关 GetRunClientByUuid,不是前端心跳。

定时任务:

java 复制代码
@Scheduled(cron = "0 * * * * ?")
public void syncEveryMinute() {
    wxworkRobotOnlineSyncService.syncAllRobotsOnlineStatus();
}

启动后再延迟 15 秒补一次,避免刚部署时库表还是旧状态。一次同步会扫全部机器人,但并发被 Semaphore 卡死:

java 复制代码
int ONLINE_CHECK_MAX_PARALLEL = 3;

校验异常且当前还标着 online,会改成 offline------宁可少占一个槽位,也不让下游把请求丢给一个已经死掉的 uuid。

反向纠偏在消息回调里:实例其实还活着,定时任务却误标离线时,工作台会停轮询、只读 MySQL。所以收到聊天回调会把该 uuid 改回 online:

java 复制代码
if (robot != null && "offline".equalsIgnoreCase(robot.getOnlineStatus())) {
    wxworkRobotService.updateOnlineStatusByUuid(uuid, "online");
}

授权槽位跟这套在线状态绑定:

javascript 复制代码
remainingLoginSlots() {
  if (this.isAdmin) return null
  return Math.max(0, lim - this.weworkOnlineRobotCount)
}

剩余为 0,「登录企业微信」禁用,tooltip 是「当前可登录数量为 0,无法创建企微机器人」。批量登录离线号时,也会先数一遍:离线数量大于剩余槽位,直接拒绝,不会先 init 再报错。


5. 现网边界

能做的:

  • 扫码上号、选登录城市、批量登录 / 退出 / 删除。
  • 在线号不能再点登录、二次验证、删除;可以账号退出、看日志。
  • 回调由 wxwork.callbackUrl 自动登记,工作台可点「重新设置回调」。
  • 每分钟同步在线;聊天回调纠正误标离线。
  • 非 admin 按「托管账号上限 − 在线数」扣槽;admin 显示无限。
  • 按号配关键字 / 口令入群 / 自动拉人 / AI;工作时间未配等于不限制。

明确没做、避免售前被问穿:

  • 不是无限并发几千号同时在线扫。 在线检测并行最多 3,登录仍是一号一码。
  • 槽位按在线数扣,不是按「曾经登录过的行数」扣。 离线会释放;列表里挂着一堆离线号,不占剩余可登录数。
  • 二次验证有时效。 -12007 或自动登录 -2007 会退回常规扫码,不会卡在「二次验证二维码获取失败」。
  • 本机 callbackUrl 第三方访问不到。 没有公网 / 隧道,工作台收不到实时消息。
  • 工作时间未设置 ≠ 下班停机。 未设置或禁用的号,自动化不受工作时间限制。
  • 工作台离线不是空壳装实时。 离线只展示缓存或已落库会话,要同步得先让号在线。

6. 落地清单

  1. 菜单做 企微账号管理,三个 Tab:账号管理 / 账号设置 / 工作时间。
  2. 登录弹窗两步:初始化、扫码;城市下拉只读代理表,不要让运营手填 IP。
  3. prepareWxworkLoginInstance 统一 init → setProxyproxyId ≤ 0127.0.0.1:1081
  4. wxwork.callbackUrl 必须是网关能 POST 到的地址,init / save 后 SetCallbackUrl
  5. saveGetRunClientByUuid 拉昵称头像,不要只信前端表单。
  6. 在线同步 cron 每分钟一次,并行最多 3;聊天回调纠正误标离线。
  7. 槽位 = account_limit − 在线数;admin 不限制;剩 0 禁用「登录企业微信」。

7. 总结

多账号托管的技术难度不在「弹出一个二维码」,而在 把城市出口、回调登记、在线真相和授权槽位做成同一套底座。极客聚合这条链路已经按这个节奏在跑:城市在系统设置里配一次,号在账号管理里扫进去,会话在工作台里聚合,掉线由定时任务和回调一起认。

如果你也在管一批企微号,被「十几个号轮流扫」「掉线半天没人发现」「会话散落在多台电脑」折磨过,欢迎在评论区留言场景(几号、要不要分城市出口、客服是否共用一个工作台)

相关推荐
天空属于哈夫克31 小时前
企业微信接口对接教程:请求、回执与幂等
企业微信
星核0penstarry1 小时前
B站AI大赛3万份投稿背后的真相:门槛下移后,新的壁垒是什么?
人工智能·个人开发·ai编程·自动写代码
不才不才不不才1 小时前
Spring 源码系列(27): @Transactional 七大失效场景与源码归因
java·后端·spring
大大大大晴天️1 小时前
从数仓建模到 AI 数据底座:重新理解数据治理的位置
大数据·数据治理
天空属于哈夫克31 小时前
企业微信开发怎么做:架构、队列与上线清单
架构·企业微信
妙码生花1 小时前
PHP 各框架下和 Go 的性能比较
前端·后端·go
随便做点啥1 小时前
32卡64G-910B4-16后端集群部署报告
java·大数据·运维
名字还没想好☜2 小时前
Python 字符编码实战:encode/decode、UnicodeDecodeError 与 open 的 encoding 坑
开发语言·后端·python·编程语言
大勇前进2 小时前
Java 线程创建的 4 种方式,优缺点对比,开发推荐写法
后端