简介:这是一套基于企业微信深度集成的开源SCRM系统设计源码,面向Java后端开发者、企业数字化运营团队及私域流量系统学习者,聚焦客户管理、营销自动化与社群裂变等核心场景,助力企业构建自主可控的私域运营中台。资源包共2000个文件,主体为1778个Java业务逻辑与服务层代码(如WeCustomerServiceImpl、WeFissionServiceImpl等),辅以212个XML配置文件(支撑MyBatis映射与Spring配置)、5个文本说明文件及4个Properties配置项,整体压缩包仅27.64MB,轻量易部署。已有862人学习下载,适合中高级开发者研究企业微信API对接、微服务模块划分与Vue3+Java全栈协同架构。代码结构分层明确,关键服务类覆盖客户、素材、群聊、朋友圈任务、红包、裂变活动等SCRM核心能力,注释详实,可直接用于教学分析、二次开发或私域系统原型验证。
1. 这不是又一个“企业微信对接Demo”:LinkWeChat 是能跑通私域裂变闭环的 SCRM 源码,不是玩具
你试过用企业微信官方 SDK 写个客户打标逻辑,结果发现「客户池」和「客户联系人」是两个隔离体系,打标后根本查不到?你调通了群活码生成,却卡在「群聊自动拉新+打标签+发欢迎语」三步联动的事务一致性上,最后靠人工补数据?别急——LinkWeChat 不是那种只封装了getAccessToken()就叫“接入企业微信”的半成品。它是一套真实跑过千人级销售团队、支撑日均 5000+ 客户触达、完整覆盖「客户获取 → 分层运营 → 社群裂变 → 任务驱动 → 数据归因」全链路的 Java 开源 SCRM 系统。2121 个 Java 文件不是堆出来的,而是按「服务粒度」拆解:WeFissionServiceImpl管裂变海报生成与扫码归因,WeMomentsTaskServiceImpl控制朋友圈任务分发与完成校验,WeQiRuleServiceImpl实现企微特有的「客户群欢迎语+自动打标签+分配销售」原子操作。它不教你怎么写 HTTP 请求,它直接告诉你:当客户扫了带参数的活码,如何在 300ms 内完成「创建外部联系人 → 绑定客户群 → 触发欢迎语模板 → 记录裂变关系 → 同步 CRM 标签」这整套事务。适合两类人:想把企业微信真正用成私域中枢的 Java 后端工程师,以及需要快速验证 SCRM 业务模型是否成立的产品/运营同学——代码即文档,跑起来就是最小可行系统。
2. 从零启动 LinkWeChat:Spring Boot + MyBatis-Plus 微服务骨架落地实操
LinkWeChat 的后端不是单体巨石,而是基于 Spring Boot 2.7.x(兼容 JDK 8/11)构建的模块化微服务架构。核心模块按业务域划分:linkwechat-customer(客户中心)、linkwechat-moments(朋友圈运营)、linkwechat-fission(裂变增长)、linkwechat-task(任务引擎)。每个模块独立部署、独立数据库,通过 Feign 调用,避免单点故障。前端 Vue3 项目通过 Nginx 反向代理到各后端服务,这种结构决定了你不能像启动普通 Spring Boot 项目那样mvn spring-boot:run就完事——必须先理清依赖拓扑和配置注入路径。
2.1 初始化数据库与表结构:MyBatis-Plus 自动生成 vs 手动建库脚本
项目未提供一键建库 SQL,但linkwechat-common/src/main/resources/mapper/下有全部 XML 映射文件,且linkwechat-customer模块的CustomerMapper.xml中明确声明了<insert>和<select>的字段映射。正确做法是:
# 1. 创建四个独立数据库(非单库多表!) mysql -u root -p -e "CREATE DATABASE linkwechat_customer DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -u root -p -e "CREATE DATABASE linkwechat_moments DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -u root -p -e "CREATE DATABASE linkwechat_fission DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -u root -p -e "CREATE DATABASE linkwechat_task DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 2. 执行各模块下的 init.sql(注意:路径需根据实际模块名调整) mysql -u root -p linkwechat_customer < linkwechat-customer/src/main/resources/sql/init_customer.sql mysql -u root -p linkwechat_moments < linkwechat-moments/src/main/resources/sql/init_moments.sql提示:
init_customer.sql中包含we_customer(客户主表)、we_customer_tag_rel(客户-标签关系表)、we_external_user(外部联系人表)等 12 张核心表;init_moments.sql则建we_moments_task(朋友圈任务表)、we_moments_task_record(执行记录表)等 8 张表。切勿跳过建库步骤直接启动,否则 MyBatis-Plus 的@TableId(type = IdType.AUTO)会因表不存在而抛出Invalid bound statement (not found)异常,而非直观的数据库连接失败。
2.2 配置企业微信凭证:Secret、AgentId、CorpId 的三级校验机制
LinkWeChat 对企业微信 API 的调用不是简单拼接 URL,而是通过WeComConfig类统一管理凭证,并内置三级校验:
- 启动时校验:
LinkWeChatApplication.java中@PostConstruct方法会检查corpid、corpsecret、agentid是否为空; - API 调用前校验:每个 Service 方法(如
WeCustomerServiceImpl.getCustomerList())开头调用WeComTokenManager.checkTokenValid(),验证 AccessToken 是否过期(默认 2 小时); - 失败重试校验:当
WeComHttpUtil.doPost()返回errcode=40014(access_token 无效)时,自动触发refreshAccessToken()并重试请求。
因此,你的application.yml必须严格填写:
linkwechat: wecom: corp-id: wwxxxxxxxxxxxxxxxx # 企业ID,注意是"ww"开头,非"1xxx" corp-secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 应用Secret,非企业Secret agent-id: 1000001 # 应用AgentId,整数,非字符串 token: your_custom_token # 用于接收事件回调的Token,任意字符串 encoding-aes-key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...... # 43 位 AES Key,必须严格 43 字符注意:
encoding-aes-key是 Base64 编码的 32 字节密钥,解码后必须是 32 字节(即 Base64 字符串长度为 43)。若填错,企业微信回调事件(如客户入群、朋友圈互动)将无法解密,日志中会持续打印Invalid signature。血泪经验:用 Python 生成时别用base64.b64encode(os.urandom(32)),它返回 bytes,需.decode();用在线工具生成时务必确认输出长度为 43。
2.3 启动服务链:Nacos 注册中心 + Feign 调用链路验证
LinkWeChat 默认使用 Nacos 作为注册中心和配置中心。启动顺序至关重要:
先启动 Nacos Server(v2.0.3):
# 下载 nacos-server-2.0.3.tar.gz 解压后 cd nacos/bin sh startup.sh -m standalone访问
http://localhost:8848/nacos,账号密码默认nacos/nacos。再启动各微服务模块(按依赖顺序):
# 1. 启动基础服务(提供通用工具类) cd linkwechat-common && mvn clean package && java -jar target/linkwechat-common-1.0.0.jar # 2. 启动客户中心(被其他模块依赖) cd ../linkwechat-customer && mvn clean package && java -jar target/linkwechat-customer-1.0.0.jar # 3. 启动任务引擎(fission 和 moments 都依赖它) cd ../linkwechat-task && mvn clean package && java -jar target/linkwechat-task-1.0.0.jar # 4. 启动裂变模块(依赖 customer 和 task) cd ../linkwechat-fission && mvn clean package && java -jar target/linkwechat-fission-1.0.0.jar # 5. 启动朋友圈模块(依赖 customer 和 task) cd ../linkwechat-moments && mvn clean package && java -jar target/linkwechat-moments-1.0.0.jar验证 Feign 调用是否通: 查看
linkwechat-fission日志,搜索FeignClient关键字,应看到类似:[WeFissionServiceImpl] calling customer service to get external user info... [WeCustomerClient] request success, status=200, body={"code":0,"data":{"userId":"zhangsan","name":"张三"}}若出现
feign.RetryableException: Connection refused,说明linkwechat-customer未注册到 Nacos,或application.yml中spring.cloud.nacos.discovery.server-addr配置错误(应为localhost:8848,非127.0.0.1:8848)。
3. 核心业务模块源码深度拆解:WeFissionServiceImpl 与 WeMomentsTaskServiceImpl 的事务边界设计
LinkWeChat 的价值不在“能调 API”,而在它如何把企业微信的原子能力组合成可落地的业务流。WeFissionServiceImpl和WeMomentsTaskServiceImpl是两个最典型的“业务编排器”,它们的代码结构直接暴露了作者对 SCRM 本质的理解:所有营销动作必须可追踪、可归因、可回滚。这不是靠数据库事务就能解决的——因为企微 API 调用是外部 HTTP 请求,失败后无法自动 rollback。
3.1 WeFissionServiceImpl:裂变海报生成与扫码归因的幂等性保障
裂变的核心是「用户 A 分享海报 → 用户 B 扫码 → 系统记录 B 是 A 的下线」。但企微不提供“扫码者自动关联分享者”的接口,必须自己实现。WeFissionServiceImpl.generateFissionQrCode()方法做了三件事:
- 生成带参数的活码:调用
WeQrCodeServiceImpl.createFissionQrCode(),传入fissionId(裂变活动ID)、userId(分享者ID),生成形如https://work.weixin.qq.com/kfid/kfc123456789?params=fissionId=1001&userId=zhangsan的链接; - 持久化裂变关系:在
we_fission_relation表中插入记录,share_user_id = 'zhangsan',fission_id = 1001,status = 'WAITING'; - 监听扫码事件:当 B 扫码,企微推送
change_external_contact事件,WeExternalContactListener.onAddExternalContact()捕获后,解析 URL 参数,更新we_fission_relation.status = 'SUCCESS'并设置scan_user_id = 'lisi'。
关键点在于幂等性控制:onAddExternalContact()方法开头有:
// 防止同一客户多次扫码触发多次归因 if (weFissionRelationMapper.selectCount(new QueryWrapper<WeFissionRelation>() .eq("scan_user_id", externalUserId) .eq("fission_id", fissionId)) > 0) { log.warn("External user {} already scanned fission activity {}", externalUserId, fissionId); return; }这段逻辑解决了真实场景中的“客户误扫多次”问题。很多开源项目忽略这点,导致一个客户被重复计入多个裂变活动,数据失真。LinkWeChat 把它写死在 Listener 里,而不是靠前端防抖——因为扫码行为不可控,必须服务端兜底。
3.2 WeMomentsTaskServiceImpl:朋友圈任务的“发布-执行-校验”状态机
朋友圈任务不是简单发条图文,而是“销售 A 发布任务 → 系统推送给指定客户群 → 客户 B 点赞/评论 → 销售 A 在后台确认完成 → 自动发放奖励”。WeMomentsTaskServiceImpl用状态机管理整个生命周期:
| 状态 | 触发动作 | 数据库字段task_status | 备注 |
|---|---|---|---|
DRAFT | 销售创建任务 | 0 | 未发布,仅存草稿 |
PUBLISHED | 调用企微send接口成功 | 1 | 任务已推送到客户群 |
EXECUTED | 监听到客户点赞事件 | 2 | 客户已互动,待人工审核 |
COMPLETED | 销售点击“确认完成” | 3 | 奖励发放,任务闭环 |
状态流转由WeMomentsTaskServiceImpl.updateTaskStatus()控制,且每次更新都检查前置状态:
public void updateTaskStatus(Long taskId, Integer newStatus) { WeMomentsTask task = weMomentsTaskMapper.selectById(taskId); // 状态迁移必须符合规则:DRAFT→PUBLISHED,PUBLISHED→EXECUTED,EXECUTED→COMPLETED if (!validStatusTransition(task.getTaskStatus(), newStatus)) { throw new BusinessException("Invalid status transition: " + task.getTaskStatus() + " -> " + newStatus); } task.setTaskStatus(newStatus); weMomentsTaskMapper.updateById(task); }这种显式状态机设计,让运营同学能清晰看到每个任务卡在哪一环。比如发现大量任务停留在
EXECUTED,就知道是销售审核环节存在瓶颈,而非技术故障。很多 SCRM 系统用布尔字段is_done,导致无法区分“已执行未审核”和“已审核完成”,数据维度丢失。
3.3 WeQiRuleServiceImpl:欢迎语+打标签+分配销售的原子操作封装
企微的「客户进群欢迎语」和「自动打标签」是两个独立接口,但业务上必须同时生效。WeQiRuleServiceImpl.handleGroupJoinEvent()将它们封装为原子操作:
@Transactional(rollbackFor = Exception.class) public void handleGroupJoinEvent(String groupId, String userId) { // 1. 发送欢迎语(调用 group/welcome_message 接口) weGroupService.sendWelcomeMessage(groupId, userId); // 2. 给该客户打标签(调用 externalcontact/add_contact_way 接口) weCustomerService.addTagToCustomer(userId, "新进群客户"); // 3. 分配给指定销售(调用 externalcontact/batch_add_to_department 接口) weCustomerService.assignToSales(userId, "sales_leader_id"); }注意:
@Transactional只保证本地数据库操作回滚,不保证企微 API 调用回滚。因此,方法内做了重试机制:若第 2 步打标签失败,会捕获WeComException并重试 2 次;若仍失败,则记录we_qi_rule_log表,标记status = 'FAILED',供人工干预。这种“尽力而为 + 人工兜底”的设计,比强一致性更符合 SaaS 场景。
4. 避坑指南:LinkWeChat 开发中最容易翻车的五个边界问题
部署 LinkWeChat 不是复制粘贴就能跑通的事。我在三家公司落地过这个项目,踩过的坑足够写本小册子。以下是最痛、最隐蔽、文档里绝不会写的五个问题,按发生频率排序:
4.1 现象:客户扫码后,we_fission_relation表无记录,日志显示No matching handler for event change_external_contact
原因:企业微信后台配置的「接收消息服务器 URL」未正确指向你的linkwechat-customer服务,或 Nginx 代理配置未透传X-WX-Nonce、X-WX-Signature等签名头。
解决:
- 检查企微管理后台「应用管理 → 自建应用 → 接收消息 → 服务器配置」中的 URL 是否为
https://your-domain.com/api/we/event(注意是/api/we/event,非/event); - 在 Nginx 配置中添加:
缺少这三行头,企微的签名验证必然失败,事件被直接丢弃。location /api/we/ { proxy_pass http://localhost:8081/; # linkwechat-customer 端口 proxy_set_header X-WX-Nonce $http_x_wx_nonce; proxy_set_header X-WX-Signature $http_x_wx_signature; proxy_set_header X-WX-Timestamp $http_x_wx_timestamp; }
4.2 现象:WeMomentsTaskServiceImpl发布任务后,客户群收不到消息,日志报errcode=40003, errmsg=invalid userid
原因:企微要求朋友圈任务的接收者必须是「客户联系人」(externalUser),而非「内部员工」(user)。但代码中WeMomentsTaskServiceImpl.publishTask()默认从sys_user表取userId,而sys_user.userId是员工 ID,不是客户 ID。
解决:
- 修改
publishTask()方法,接收参数改为List<String> externalUserIds(客户外部联系人 ID 列表); - 前端在选择接收人群时,必须调用
WeCustomerServiceImpl.listExternalUsersByGroupId()获取客户 ID 列表,而非SysUserServiceImpl.listAllUsers(); - 数据库
we_moments_task_receiver表的receiver_id字段类型应为VARCHAR(64),存储wm_xxx格式的客户 ID。
4.3 现象:WeRedEnvelopesServiceImpl发红包失败,日志显示errcode=40001, errmsg=invalid credential
原因:企微红包接口send_red_packet要求使用「红包专用 Secret」,而非应用 Secret。很多开发者直接复用corp-secret,导致鉴权失败。
解决:
- 登录企微管理后台,进入「应用管理 → 自建应用 → 红包 → 红包配置」,开启红包功能并获取「红包 Secret」;
- 在
application.yml中新增配置:linkwechat: wecom: red-envelope-secret: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 红包专用Secret - 修改
WeRedEnvelopesServiceImpl.sendRedEnvelope(),使用该 Secret 生成 AccessToken。
4.4 现象:WeGroupServiceImpl创建客户群后,群聊 IDchatid无法用于后续接口(如发欢迎语),返回errcode=40011
原因:企微的chatid是加密字符串,不同应用调用create_group_chat返回的chatid格式不同。LinkWeChat 默认使用agentid生成,但若你启用了「客户联系」权限,必须用corpid生成。
解决:
- 检查企微后台「客户联系 → 客户联系权限 → 应用权限」是否勾选「创建客户群」;
- 修改
WeGroupServiceImpl.createGroupChat(),将agentid参数替换为corpid:// 原代码(错误) String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/appchat/create?access_token=%s", accessToken); // 正确代码(需传 corpid) String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/appchat/create?access_token=%s&corpid=%s", accessToken, corpId);
4.5 现象:SysUserServiceImpl登录后,前端 Vue3 页面显示401 Unauthorized,但后端日志无异常
原因:Vue3 前端使用axios发送请求时,默认不携带 Cookie,而 LinkWeChat 的登录态基于JSESSIONIDCookie。
解决:
- 在
src/utils/request.js中全局配置:axios.defaults.withCredentials = true; // 关键! - 后端
linkwechat-common的CorsConfig.java中,allowedOrigins必须是具体域名(如https://your-front-end.com),不能是*,否则浏览器拒绝发送 Cookie。
5. 进阶技巧:用 WeTasksServiceImpl 实现“销售每日打卡+客户跟进”双目标自动化
LinkWeChat 的WeTasksServiceImpl模块常被低估——它不只是发个待办提醒,而是能驱动销售行为的数据引擎。我把它改造成“销售行为仪表盘”的核心,实现了两个关键效果:1)销售每天必须完成 3 个客户跟进才解锁当日奖金;2)未完成的跟进自动升级为上级督办任务。这不是靠增加按钮实现的,而是通过任务状态机 + 时间窗口 + 权限继承的组合拳。
5.1 构建“客户跟进”任务模板:动态参数注入与超时自动升级
首先,在WeTasksServiceImpl中定义一个FOLLOW_UP_TASK类型的任务模板:
// WeTasksServiceImpl.java public void createFollowUpTask(String salesUserId, String customerId, LocalDateTime deadline) { WeTasks task = new WeTasks(); task.setTaskType("FOLLOW_UP_TASK"); task.setCreatorId(salesUserId); task.setAssigneeId(salesUserId); // 初始分配给自己 task.setDeadline(deadline); // 截止时间设为今日23:59 task.setStatus(TaskStatus.PENDING.getCode()); // 动态注入客户信息(避免硬编码) Map<String, Object> params = new HashMap<>(); params.put("customerId", customerId); params.put("customerName", getCustomerName(customerId)); // 从 we_customer 表查 task.setParams(JSON.toJSONString(params)); weTasksMapper.insert(task); // 设置定时任务:若 deadline 过期且 status != COMPLETED,则升级 scheduleTaskUpgrade(task.getId(), deadline.plusSeconds(1)); }scheduleTaskUpgrade()使用@Scheduled注解,但关键在升级逻辑:
@Scheduled(fixedDelay = 60000) // 每分钟扫描一次 public void checkOverdueTasks() { LocalDateTime now = LocalDateTime.now(); List<WeTasks> overdueTasks = weTasksMapper.selectList( new QueryWrapper<WeTasks>() .eq("task_type", "FOLLOW_UP_TASK") .eq("status", TaskStatus.PENDING.getCode()) .lt("deadline", now) ); for (WeTasks task : overdueTasks) { // 查询销售的直属上级(通过 sys_user 表的 parent_id 字段) SysUser salesUser = sysUserMapper.selectById(task.getAssigneeId()); SysUser manager = sysUserMapper.selectById(salesUser.getParentId()); // 创建督办任务,assigneeId 设为 manager.getId() WeTasks upgradeTask = new WeTasks(); upgradeTask.setTaskType("SUPERVISION_TASK"); upgradeTask.setCreatorId(task.getAssigneeId()); // 原销售 upgradeTask.setAssigneeId(manager.getId()); // 上级 upgradeTask.setParams(task.getParams()); // 复用原参数 upgradeTask.setRemark("【自动升级】客户跟进超时,请督办"); weTasksMapper.insert(upgradeTask); // 更新原任务状态 task.setStatus(TaskStatus.UPGRADED.getCode()); weTasksMapper.updateById(task); } }5.2 前端 Vue3 的“打卡进度条”:实时聚合与状态联动
在销售个人首页,我们用el-progress显示当日完成度。数据来自后端/api/task/todayProgress接口:
// src/api/task.js export function getTodayProgress() { return request({ url: '/api/task/todayProgress', method: 'get', withCredentials: true // 确保携带 JSESSIONID }) }后端接口逻辑:
// WeTasksServiceImpl.java @GetMapping("/todayProgress") public Result<Map<String, Object>> getTodayProgress(@AuthenticationPrincipal SysUser user) { LocalDateTime start = LocalDate.now().atStartOfDay(); LocalDateTime end = LocalDate.now().atTime(23, 59, 59); // 统计今日创建的 FOLLOW_UP_TASK 数量(目标值) int total = weTasksMapper.selectCount( new QueryWrapper<WeTasks>() .eq("task_type", "FOLLOW_UP_TASK") .eq("creator_id", user.getUserId()) .between("create_time", start, end) ); // 统计今日已完成的数量 int completed = weTasksMapper.selectCount( new QueryWrapper<WeTasks>() .eq("task_type", "FOLLOW_UP_TASK") .eq("assignee_id", user.getUserId()) .eq("status", TaskStatus.COMPLETED.getCode()) .between("update_time", start, end) ); Map<String, Object> data = new HashMap<>(); data.put("total", total); data.put("completed", completed); data.put("progress", total > 0 ? (int) Math.round((double) completed / total * 100) : 0); data.put("canClaimBonus", completed >= 3); // 达标解锁奖金 return Result.success(data); }这个接口的关键是
@AuthenticationPrincipal SysUser user,它从 Spring Security 的SecurityContext中提取当前登录用户,无需前端传userId,杜绝了 ID 伪造风险。而canClaimBonus字段直接驱动前端按钮状态:达标时显示“领取奖金”,未达标时显示“继续跟进”。
5.3 数据看板:用 WeGroupServiceImpl 关联任务与社群活跃度
最后一步,把销售行为和客户行为打通。我们在WeGroupServiceImpl的getGroupStats()方法中,加入任务完成率统计:
// WeGroupServiceImpl.java public GroupStats getGroupStats(String chatId) { GroupStats stats = new GroupStats(); stats.setChatId(chatId); // 基础群数据 WeGroup group = weGroupMapper.selectOne(new QueryWrapper<WeGroup>().eq("chat_id", chatId)); stats.setMemberCount(group.getMemberCount()); // 关联任务完成率:统计该群内客户被分配的 FOLLOW_UP_TASK 完成率 List<String> customerIds = weGroupCustomerRelMapper.selectCustomerIdList(chatId); if (!customerIds.isEmpty()) { int totalTasks = weTasksMapper.selectCount( new QueryWrapper<WeTasks>() .in("params", customerIds.stream().map(id -> "{\"customerId\":\"" + id + "\"}").collect(Collectors.toList())) .eq("task_type", "FOLLOW_UP_TASK") ); int completedTasks = weTasksMapper.selectCount( new QueryWrapper<WeTasks>() .in("params", customerIds.stream().map(id -> "{\"customerId\":\"" + id + "\"}").collect(Collectors.toList())) .eq("task_type", "FOLLOW_UP_TASK") .eq("status", TaskStatus.COMPLETED.getCode()) ); stats.setTaskCompletionRate(totalTasks > 0 ? (double) completedTasks / totalTasks : 0.0); } return stats; }这样,运营总监在后台看“XX客户群”详情页时,不仅能看见群人数、发言频次,还能看到“该群客户被销售跟进的完成率”,从而判断:是销售懈怠?还是客户质量差?数据不再割裂。
从那以后我每次做 SCRM 项目,都强制走一遍WeTasksServiceImpl的任务状态机设计——不是为了炫技,而是因为真正的私域运营,从来不是“发多少条消息”,而是“有多少动作被闭环”。LinkWeChat 把这个闭环的骨架,已经用 Java 写得明明白白。希望帮到你。
本文还有配套的精品资源,点击获取