客服模块

This commit is contained in:
2025-12-19 14:37:29 +08:00
parent 409e33abb6
commit 5f2301e16c
42 changed files with 3012 additions and 198 deletions

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.xyzh</groupId>
<artifactId>common</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh.common</groupId>
<artifactId>common-wechat</artifactId>
<version>${urban-lifeline.version}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- 用于获取微信客服的系统配置 -->
<dependency>
<groupId>org.xyzh.apis</groupId>
<artifactId>api-system</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,31 @@
package org.xyzh.common.wechat.config;
import lombok.Data;
/**
* @description 微信客服配置
* @filename WeChatConfig.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
public class WeChatKefuConfig {
private String corpId;
private String secret;
private String token;
private String encodingAesKey;
private String openKfid;
private String welcomeTemplate;
private String accessToken;
private Long accessTokenExpireTime;
}

View File

@@ -0,0 +1,69 @@
package org.xyzh.common.wechat.config;
import org.apache.dubbo.config.annotation.DubboReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.xyzh.api.system.service.SysConfigService;
import org.xyzh.common.core.domain.ResultDomain;
import jakarta.annotation.PostConstruct;
/**
* @description 微信初始化配置,从系统配置中加载微信客服参数
* @filename WeChatInit.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Component
public class WeChatKefuInit {
private static final Logger logger = LoggerFactory.getLogger(WeChatKefuInit.class);
@DubboReference(version = "1.0.0", group = "system", check = false)
private SysConfigService sysConfigService;
private static WeChatKefuConfig weChatConfig;
@PostConstruct
public void init() {
logger.info("初始化微信客服配置...");
weChatConfig = new WeChatKefuConfig();
loadConfig();
}
public void loadConfig() {
try {
weChatConfig.setCorpId(getConfigValue("wechat.kefu.corpId"));
weChatConfig.setSecret(getConfigValue("wechat.kefu.secret"));
weChatConfig.setToken(getConfigValue("wechat.kefu.token"));
weChatConfig.setEncodingAesKey(getConfigValue("wechat.kefu.encodingAesKey"));
weChatConfig.setOpenKfid(getConfigValue("wechat.kefu.openKfid"));
weChatConfig.setWelcomeTemplate(getConfigValue("wechat.kefu.welcomeTemplate"));
logger.info("微信客服配置加载完成: corpId={}, openKfid={}", weChatConfig.getCorpId(), weChatConfig.getOpenKfid());
} catch (Exception e) {
logger.error("加载微信客服配置失败", e);
}
}
private String getConfigValue(String key) {
if (sysConfigService == null) {
logger.warn("SysConfigService 未注入,跳过配置加载: {}", key);
return null;
}
ResultDomain<String> result = sysConfigService.getConfigValueByKey(key);
if (result != null && result.getSuccess()) {
return result.getData();
}
return null;
}
public static WeChatKefuConfig getConfig() {
return weChatConfig;
}
public void refreshConfig() {
loadConfig();
}
}

View File

@@ -0,0 +1,96 @@
package org.xyzh.common.wechat.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xyzh.common.wechat.pojo.kefu.KefuCallback;
/**
* @description 客服回调默认处理器
* @filename DefaultKefuCallbackHandler.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
public class DefaultKefuCallbackHandler implements KefuCallbackHandler {
private static final Logger logger = LoggerFactory.getLogger(DefaultKefuCallbackHandler.class);
@Override
public void onEnterSession(KefuCallback callback) {
logger.info("用户进入会话: externalUserid={}, openKfid={}, welcomeCode={}",
callback.getExternalUserid(), callback.getOpenKfid(), callback.getWelcomeCode());
}
@Override
public void onMsgSendFail(KefuCallback callback) {
logger.warn("消息发送失败: failMsgid={}, failType={}",
callback.getFailMsgid(), callback.getFailType());
}
@Override
public void onUserRecallMsg(KefuCallback callback) {
logger.info("用户撤回消息: msgid={}", callback.getMsgid());
}
@Override
public void onServicerStatusChange(KefuCallback callback) {
logger.info("接待人员状态变更: openKfid={}", callback.getOpenKfid());
}
@Override
public void onSessionStatusChange(KefuCallback callback) {
logger.info("会话状态变更: openKfid={}, externalUserid={}",
callback.getOpenKfid(), callback.getExternalUserid());
}
@Override
public void onTextMessage(KefuCallback callback) {
logger.info("收到文本消息: externalUserid={}, content={}",
callback.getExternalUserid(), callback.getContent());
}
@Override
public void onImageMessage(KefuCallback callback) {
logger.info("收到图片消息: externalUserid={}, mediaId={}",
callback.getExternalUserid(), callback.getMediaId());
}
@Override
public void onVoiceMessage(KefuCallback callback) {
logger.info("收到语音消息: externalUserid={}, mediaId={}",
callback.getExternalUserid(), callback.getMediaId());
}
@Override
public void onVideoMessage(KefuCallback callback) {
logger.info("收到视频消息: externalUserid={}, mediaId={}",
callback.getExternalUserid(), callback.getMediaId());
}
@Override
public void onFileMessage(KefuCallback callback) {
logger.info("收到文件消息: externalUserid={}, mediaId={}",
callback.getExternalUserid(), callback.getMediaId());
}
@Override
public void onLocationMessage(KefuCallback callback) {
logger.info("收到位置消息: externalUserid={}", callback.getExternalUserid());
}
@Override
public void onLinkMessage(KefuCallback callback) {
logger.info("收到链接消息: externalUserid={}", callback.getExternalUserid());
}
@Override
public void onBusinessCardMessage(KefuCallback callback) {
logger.info("收到名片消息: externalUserid={}", callback.getExternalUserid());
}
@Override
public void onMiniprogramMessage(KefuCallback callback) {
logger.info("收到小程序消息: externalUserid={}", callback.getExternalUserid());
}
}

View File

@@ -0,0 +1,42 @@
package org.xyzh.common.wechat.handler;
import org.xyzh.common.wechat.pojo.kefu.KefuCallback;
/**
* @description 客服回调处理接口
* @filename KefuCallbackHandler.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
public interface KefuCallbackHandler {
void onEnterSession(KefuCallback callback);
void onMsgSendFail(KefuCallback callback);
void onUserRecallMsg(KefuCallback callback);
void onServicerStatusChange(KefuCallback callback);
void onSessionStatusChange(KefuCallback callback);
void onTextMessage(KefuCallback callback);
void onImageMessage(KefuCallback callback);
void onVoiceMessage(KefuCallback callback);
void onVideoMessage(KefuCallback callback);
void onFileMessage(KefuCallback callback);
void onLocationMessage(KefuCallback callback);
void onLinkMessage(KefuCallback callback);
void onBusinessCardMessage(KefuCallback callback);
void onMiniprogramMessage(KefuCallback callback);
}

View File

@@ -0,0 +1,28 @@
package org.xyzh.common.wechat.kefu.account;
/**
* @description 客服账号管理 Handler 接口
* 业务模块可实现此接口处理账号变更事件
* @filename KefuAccountHandler.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
public interface KefuAccountHandler {
/**
* 客服账号添加成功
*/
default void onAccountAdded(String openKfid, String name) {}
/**
* 客服账号删除成功
*/
default void onAccountDeleted(String openKfid) {}
/**
* 客服账号修改成功
*/
default void onAccountUpdated(String openKfid, String name) {}
}

View File

@@ -0,0 +1,183 @@
package org.xyzh.common.wechat.kefu.account;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.xyzh.common.wechat.kefu.core.KefuAccessTokenManager;
import org.xyzh.common.wechat.pojo.kefu.KefuAccount;
import org.xyzh.common.wechat.pojo.kefu.WeChatResponse;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
/**
* @description 客服账号管理服务
* - 添加客服账号
* - 删除客服账号
* - 修改客服账号
* - 获取客服账号列表
* - 获取客服账号链接
* @filename KefuAccountService.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Service
public class KefuAccountService {
private static final Logger logger = LoggerFactory.getLogger(KefuAccountService.class);
private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin";
private final RestTemplate restTemplate = new RestTemplate();
private final KefuAccessTokenManager tokenManager;
private KefuAccountHandler handler;
public KefuAccountService(KefuAccessTokenManager tokenManager) {
this.tokenManager = tokenManager;
}
public void setHandler(KefuAccountHandler handler) {
this.handler = handler;
}
/**
* 添加客服账号
*/
public String addAccount(String name, String mediaId) {
String accessToken = tokenManager.getAccessToken();
String url = BASE_URL + "/kf/account/add?access_token=" + accessToken;
JSONObject body = new JSONObject();
body.put("name", name);
if (mediaId != null) {
body.put("media_id", mediaId);
}
String response = postJson(url, body.toJSONString());
JSONObject result = JSON.parseObject(response);
if (result.getIntValue("errcode") == 0) {
String openKfid = result.getString("open_kfid");
logger.info("添加客服账号成功: name={}, openKfid={}", name, openKfid);
if (handler != null) {
handler.onAccountAdded(openKfid, name);
}
return openKfid;
}
logger.error("添加客服账号失败: {}", response);
return null;
}
/**
* 删除客服账号
*/
public boolean deleteAccount(String openKfid) {
String accessToken = tokenManager.getAccessToken();
String url = BASE_URL + "/kf/account/del?access_token=" + accessToken;
JSONObject body = new JSONObject();
body.put("open_kfid", openKfid);
String response = postJson(url, body.toJSONString());
JSONObject result = JSON.parseObject(response);
boolean success = result.getIntValue("errcode") == 0;
if (success) {
logger.info("删除客服账号成功: openKfid={}", openKfid);
if (handler != null) {
handler.onAccountDeleted(openKfid);
}
} else {
logger.error("删除客服账号失败: {}", response);
}
return success;
}
/**
* 修改客服账号
*/
public boolean updateAccount(String openKfid, String name, String mediaId) {
String accessToken = tokenManager.getAccessToken();
String url = BASE_URL + "/kf/account/update?access_token=" + accessToken;
JSONObject body = new JSONObject();
body.put("open_kfid", openKfid);
if (name != null) {
body.put("name", name);
}
if (mediaId != null) {
body.put("media_id", mediaId);
}
String response = postJson(url, body.toJSONString());
JSONObject result = JSON.parseObject(response);
boolean success = result.getIntValue("errcode") == 0;
if (success) {
logger.info("修改客服账号成功: openKfid={}", openKfid);
if (handler != null) {
handler.onAccountUpdated(openKfid, name);
}
} else {
logger.error("修改客服账号失败: {}", response);
}
return success;
}
/**
* 获取客服账号列表
*/
public List<KefuAccount> getAccountList() {
String accessToken = tokenManager.getAccessToken();
String url = BASE_URL + "/kf/account/list?access_token=" + accessToken;
String response = restTemplate.getForObject(url, String.class);
JSONObject result = JSON.parseObject(response);
if (result.getIntValue("errcode") == 0) {
return result.getList("account_list", KefuAccount.class);
}
logger.error("获取客服账号列表失败: {}", response);
return null;
}
/**
* 获取客服账号链接
*/
public String getAccountLink(String openKfid, String scene) {
String accessToken = tokenManager.getAccessToken();
String url = BASE_URL + "/kf/add_contact_way?access_token=" + accessToken;
JSONObject body = new JSONObject();
body.put("open_kfid", openKfid);
if (scene != null) {
body.put("scene", scene);
}
String response = postJson(url, body.toJSONString());
JSONObject result = JSON.parseObject(response);
if (result.getIntValue("errcode") == 0) {
return result.getString("url");
}
logger.error("获取客服账号链接失败: {}", response);
return null;
}
private String postJson(String url, String json) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(json, headers);
return restTemplate.postForObject(url, entity, String.class);
}
}

View File

@@ -0,0 +1,109 @@
package org.xyzh.common.wechat.kefu.core;
import org.apache.dubbo.config.annotation.DubboReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.xyzh.api.system.service.SysConfigService;
import org.xyzh.common.core.domain.ResultDomain;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import jakarta.annotation.PostConstruct;
/**
* @description 微信客服 AccessToken 管理器
* 负责获取和刷新 access_token
* @filename KefuAccessTokenManager.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Component
public class KefuAccessTokenManager {
private static final Logger logger = LoggerFactory.getLogger(KefuAccessTokenManager.class);
private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin";
private final RestTemplate restTemplate = new RestTemplate();
@DubboReference(version = "1.0.0", group = "system", check = false)
private SysConfigService sysConfigService;
private String corpId;
private String secret;
private String accessToken;
private Long accessTokenExpireTime;
@PostConstruct
public void init() {
loadConfig();
}
public void loadConfig() {
corpId = getConfigValue("wechat.kefu.corpId");
secret = getConfigValue("wechat.kefu.secret");
logger.info("微信客服配置加载完成: corpId={}", corpId);
}
private String getConfigValue(String key) {
if (sysConfigService == null) {
logger.warn("SysConfigService 未注入,跳过配置加载: {}", key);
return null;
}
ResultDomain<String> result = sysConfigService.getConfigValueByKey(key);
if (result != null && result.getSuccess()) {
return result.getData();
}
return null;
}
/**
* 获取 access_token如果过期自动刷新
*/
public String getAccessToken() {
if (accessToken != null && accessTokenExpireTime != null
&& System.currentTimeMillis() < accessTokenExpireTime) {
return accessToken;
}
return refreshAccessToken();
}
/**
* 刷新 access_token
*/
public String refreshAccessToken() {
if (corpId == null || secret == null) {
logger.error("微信配置不完整无法获取access_token");
return null;
}
String url = BASE_URL + "/gettoken?corpid=" + corpId + "&corpsecret=" + secret;
try {
String response = restTemplate.getForObject(url, String.class);
JSONObject result = JSON.parseObject(response);
if (result.getIntValue("errcode") == 0) {
accessToken = result.getString("access_token");
int expiresIn = result.getIntValue("expires_in");
accessTokenExpireTime = System.currentTimeMillis() + (expiresIn - 200) * 1000L;
logger.info("获取access_token成功有效期: {}秒", expiresIn);
return accessToken;
} else {
logger.error("获取access_token失败: {}", response);
return null;
}
} catch (Exception e) {
logger.error("获取access_token异常", e);
return null;
}
}
public String getCorpId() {
return corpId;
}
}

View File

@@ -0,0 +1,20 @@
package org.xyzh.common.wechat.kefu.info;
import org.xyzh.common.wechat.pojo.kefu.KefuCustomer;
/**
* @description 基础信息获取 Handler 接口
* 业务模块可实现此接口处理客户信息获取事件
* @filename KefuInfoHandler.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
public interface KefuInfoHandler {
/**
* 客户信息获取成功
*/
default void onCustomerInfoFetched(KefuCustomer customer) {}
}

View File

@@ -0,0 +1,100 @@
package org.xyzh.common.wechat.kefu.info;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.xyzh.common.wechat.kefu.core.KefuAccessTokenManager;
import org.xyzh.common.wechat.pojo.kefu.KefuCustomer;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
/**
* @description 其他基础信息获取服务
* - 获取客户基础信息
* - 获取企业状态信息
* @filename KefuInfoService.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Service
public class KefuInfoService {
private static final Logger logger = LoggerFactory.getLogger(KefuInfoService.class);
private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin";
private final RestTemplate restTemplate = new RestTemplate();
private final KefuAccessTokenManager tokenManager;
private KefuInfoHandler handler;
public KefuInfoService(KefuAccessTokenManager tokenManager) {
this.tokenManager = tokenManager;
}
public void setHandler(KefuInfoHandler handler) {
this.handler = handler;
}
/**
* 获取客户基础信息
*/
public List<KefuCustomer> getCustomerInfo(List<String> externalUseridList) {
String accessToken = tokenManager.getAccessToken();
String url = BASE_URL + "/kf/customer/batchget?access_token=" + accessToken;
JSONObject body = new JSONObject();
body.put("external_userid_list", externalUseridList);
String response = postJson(url, body.toJSONString());
JSONObject result = JSON.parseObject(response);
if (result.getIntValue("errcode") == 0) {
List<KefuCustomer> customers = result.getList("customer_list", KefuCustomer.class);
if (handler != null && customers != null) {
for (KefuCustomer customer : customers) {
handler.onCustomerInfoFetched(customer);
}
}
return customers;
}
logger.error("获取客户信息失败: {}", response);
return null;
}
/**
* 获取企业状态信息
*/
public JSONObject getCorpStatistic() {
String accessToken = tokenManager.getAccessToken();
String url = BASE_URL + "/kf/get_corp_statistic?access_token=" + accessToken;
JSONObject body = new JSONObject();
// 可添加时间范围等参数
String response = postJson(url, body.toJSONString());
JSONObject result = JSON.parseObject(response);
if (result.getIntValue("errcode") == 0) {
return result.getJSONObject("statistic");
}
logger.error("获取企业状态信息失败: {}", response);
return null;
}
private String postJson(String url, String json) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(json, headers);
return restTemplate.postForObject(url, entity, String.class);
}
}

View File

@@ -0,0 +1,51 @@
package org.xyzh.common.wechat.kefu.message;
import org.xyzh.common.wechat.pojo.kefu.KefuSyncMsgResponse.KefuSyncMsg;
/**
* @description 客服消息处理 Handler 接口
* 业务模块实现此接口处理客服消息和事件
* @filename KefuMessageHandler.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
public interface KefuMessageHandler {
/**
* 用户进入会话事件
* @param openKfid 客服账号ID
* @param externalUserid 用户ID
* @param scene 场景值
* @param sceneParam 场景参数可传递工单ID等
* @param welcomeCode 欢迎语code
*/
void onEnterSession(String openKfid, String externalUserid, String scene, String sceneParam, String welcomeCode);
/**
* 收到文本消息
*/
void onTextMessage(String openKfid, String externalUserid, String msgid, String content, Long sendTime);
/**
* 收到图片消息
*/
void onImageMessage(String openKfid, String externalUserid, String msgid, String mediaId, Long sendTime);
/**
* 会话状态变更
*/
void onSessionStatusChange(String openKfid, String externalUserid, String changeType,
String oldServicerUserid, String newServicerUserid);
/**
* 消息发送失败
*/
void onMsgSendFail(String openKfid, String externalUserid, String failMsgid, String failType);
/**
* 其他消息(语音、视频、文件等)
*/
default void onOtherMessage(KefuSyncMsg msg) {}
}

View File

@@ -0,0 +1,223 @@
package org.xyzh.common.wechat.kefu.message;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.xyzh.common.wechat.kefu.core.KefuAccessTokenManager;
import org.xyzh.common.wechat.pojo.kefu.KefuSyncMsgResponse;
import org.xyzh.common.wechat.pojo.kefu.KefuSyncMsgResponse.KefuSyncMsg;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
/**
* @description 客服消息收发服务
* - 接收消息和事件(同步消息)
* - 发送消息
* - 发送客服欢迎语
* - 撤回消息
* @filename KefuMessageService.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Service
public class KefuMessageService {
private static final Logger logger = LoggerFactory.getLogger(KefuMessageService.class);
private static final String BASE_URL = "https://qyapi.weixin.qq.com/cgi-bin";
private final RestTemplate restTemplate = new RestTemplate();
private final KefuAccessTokenManager tokenManager;
private KefuMessageHandler handler;
public KefuMessageService(KefuAccessTokenManager tokenManager) {
this.tokenManager = tokenManager;
}
public void setHandler(KefuMessageHandler handler) {
this.handler = handler;
}
// ========================= 接收消息和事件 =========================
/**
* 同步消息(拉取新消息)
*/
public KefuSyncMsgResponse syncMessages(String cursor, String token, Integer limit) {
String accessToken = tokenManager.getAccessToken();
String url = BASE_URL + "/kf/sync_msg?access_token=" + accessToken;
JSONObject body = new JSONObject();
if (cursor != null && !cursor.isEmpty()) {
body.put("cursor", cursor);
}
if (token != null && !token.isEmpty()) {
body.put("token", token);
}
if (limit != null) {
body.put("limit", limit);
}
String response = postJson(url, body.toJSONString());
return JSON.parseObject(response, KefuSyncMsgResponse.class);
}
/**
* 处理同步消息(模板方法)
*/
public void processMessages(KefuSyncMsgResponse response) {
if (response == null || !response.isSuccess()) {
logger.error("消息同步响应异常: {}", response != null ? response.getErrmsg() : "null");
return;
}
List<KefuSyncMsg> msgList = response.getMsgList();
if (msgList == null || msgList.isEmpty()) {
return;
}
logger.info("开始处理 {} 条消息", msgList.size());
for (KefuSyncMsg msg : msgList) {
try {
processMessage(msg);
} catch (Exception e) {
logger.error("处理消息异常: msgid={}", msg.getMsgid(), e);
}
}
}
protected void processMessage(KefuSyncMsg msg) {
if (handler == null) {
logger.warn("未设置消息处理器");
return;
}
String msgtype = msg.getMsgtype();
if ("event".equals(msgtype)) {
processEvent(msg);
} else if ("text".equals(msgtype)) {
String content = msg.getText() != null ? msg.getText().getContent() : "";
handler.onTextMessage(msg.getOpenKfid(), msg.getExternalUserid(), msg.getMsgid(), content, msg.getSendTime());
} else if ("image".equals(msgtype)) {
String mediaId = msg.getImage() != null ? msg.getImage().getMediaId() : "";
handler.onImageMessage(msg.getOpenKfid(), msg.getExternalUserid(), msg.getMsgid(), mediaId, msg.getSendTime());
} else {
handler.onOtherMessage(msg);
}
}
protected void processEvent(KefuSyncMsg msg) {
if (msg.getEvent() == null || handler == null) {
return;
}
String eventType = msg.getEvent().getEventType();
String openKfid = msg.getEvent().getOpenKfid();
String externalUserid = msg.getEvent().getExternalUserid();
switch (eventType) {
case "enter_session":
handler.onEnterSession(openKfid, externalUserid, msg.getEvent().getScene(),
msg.getEvent().getSceneParam(), msg.getEvent().getWelcomeCode());
break;
case "msg_send_fail":
handler.onMsgSendFail(openKfid, externalUserid, msg.getEvent().getFailMsgid(), msg.getEvent().getFailType());
break;
case "session_status_change":
handler.onSessionStatusChange(openKfid, externalUserid, msg.getEvent().getChangeType(),
msg.getEvent().getOldServicerUserid(), msg.getEvent().getNewServicerUserid());
break;
default:
logger.debug("未处理的事件类型: {}", eventType);
break;
}
}
// ========================= 发送消息 =========================
/**
* 发送文本消息
*/
public String sendTextMessage(String touser, String openKfid, String content) {
String accessToken = tokenManager.getAccessToken();
String url = BASE_URL + "/kf/send_msg?access_token=" + accessToken;
JSONObject body = new JSONObject();
body.put("touser", touser);
body.put("open_kfid", openKfid);
body.put("msgtype", "text");
JSONObject text = new JSONObject();
text.put("content", content);
body.put("text", text);
String response = postJson(url, body.toJSONString());
JSONObject result = JSON.parseObject(response);
if (result.getIntValue("errcode") == 0) {
return result.getString("msgid");
}
logger.error("发送消息失败: {}", response);
return null;
}
/**
* 发送客服欢迎语
*/
public String sendWelcomeMessage(String code, String content) {
String accessToken = tokenManager.getAccessToken();
String url = BASE_URL + "/kf/send_msg_on_event?access_token=" + accessToken;
JSONObject body = new JSONObject();
body.put("code", code);
body.put("msgtype", "text");
JSONObject text = new JSONObject();
text.put("content", content);
body.put("text", text);
String response = postJson(url, body.toJSONString());
JSONObject result = JSON.parseObject(response);
if (result.getIntValue("errcode") == 0) {
return result.getString("msgid");
}
logger.error("发送欢迎语失败: {}", response);
return null;
}
/**
* 撤回消息
*/
public boolean recallMessage(String msgid) {
String accessToken = tokenManager.getAccessToken();
String url = BASE_URL + "/kf/recall_msg?access_token=" + accessToken;
JSONObject body = new JSONObject();
body.put("msgid", msgid);
String response = postJson(url, body.toJSONString());
JSONObject result = JSON.parseObject(response);
return result.getIntValue("errcode") == 0;
}
private String postJson(String url, String json) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(json, headers);
return restTemplate.postForObject(url, entity, String.class);
}
}

View File

@@ -0,0 +1,21 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @description access_token响应
* @filename AccessTokenResponse.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class AccessTokenResponse extends WeChatResponse {
private String accessToken;
private Integer expiresIn;
}

View File

@@ -0,0 +1,26 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
import java.util.List;
/**
* @description 客服账号
* @filename KefuAccount.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
public class KefuAccount {
private String openKfid;
private String name;
private String avatar;
private String managePrivilege;
private List<String> receptionistIdList;
}

View File

@@ -0,0 +1,19 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @description 添加客服账号响应
* @filename KefuAccountAddResponse.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class KefuAccountAddResponse extends WeChatResponse {
private String openKfid;
}

View File

@@ -0,0 +1,20 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* @description 客服账号列表响应
* @filename KefuAccountListResponse.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class KefuAccountListResponse extends WeChatResponse {
private List<KefuAccount> accountList;
}

View File

@@ -0,0 +1,47 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
/**
* @description 客服回调消息
* @filename KefuCallback.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
public class KefuCallback {
private String toUserName;
private String createTime;
private String msgType;
private String event;
private String token;
private String openKfid;
private String externalUserid;
private String scene;
private String sceneParam;
private String welcomeCode;
private String failMsgid;
private String failType;
private String msgid;
private String content;
private String picUrl;
private String mediaId;
}

View File

@@ -0,0 +1,25 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
/**
* @description 客服客户信息
* @filename KefuCustomer.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
public class KefuCustomer {
private String externalUserid;
private String nickname;
private String avatar;
private Integer gender;
private String unionid;
}

View File

@@ -0,0 +1,22 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* @description 获取客户信息响应
* @filename KefuCustomerResponse.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class KefuCustomerResponse extends WeChatResponse {
private List<KefuCustomer> customerList;
private String invalidExternalUserid;
}

View File

@@ -0,0 +1,29 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
/**
* @description 客服事件
* @filename KefuEvent.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
public class KefuEvent {
private String token;
private String openKfid;
private String externalUserid;
private String scene;
private String sceneParam;
private String welcomeCode;
private Long wechatChannels;
}

View File

@@ -0,0 +1,19 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @description 获取客服链接响应
* @filename KefuLinkResponse.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class KefuLinkResponse extends WeChatResponse {
private String url;
}

View File

@@ -0,0 +1,57 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
/**
* @description 客服消息
* @filename KefuMessage.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
public class KefuMessage {
private String touser;
private String openKfid;
private String msgid;
private String msgtype;
private TextContent text;
private ImageContent image;
private LinkContent link;
private MiniprogramContent miniprogram;
@Data
public static class TextContent {
private String content;
}
@Data
public static class ImageContent {
private String mediaId;
}
@Data
public static class LinkContent {
private String title;
private String desc;
private String url;
private String thumbMediaId;
}
@Data
public static class MiniprogramContent {
private String appid;
private String pagepath;
private String title;
private String thumbMediaId;
}
}

View File

@@ -0,0 +1,19 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @description 发送消息响应
* @filename KefuSendMsgResponse.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class KefuSendMsgResponse extends WeChatResponse {
private String msgid;
}

View File

@@ -0,0 +1,139 @@
package org.xyzh.common.wechat.pojo.kefu;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @description 同步消息响应
* @filename KefuSyncMsgResponse.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class KefuSyncMsgResponse extends WeChatResponse {
private String nextCursor;
private Integer hasMore;
private List<KefuSyncMsg> msgList;
@Data
public static class KefuSyncMsg {
private String msgid;
private String openKfid;
private String externalUserid;
private Long sendTime;
private Integer origin;
private String servicerUserid;
private String msgtype;
private TextContent text;
private ImageContent image;
private VoiceContent voice;
private VideoContent video;
private FileContent file;
private LocationContent location;
private LinkContent link;
private BusinessCardContent businessCard;
private MiniprogramContent miniprogram;
private EventContent event;
}
@Data
public static class TextContent {
private String content;
private String menuId;
}
@Data
public static class ImageContent {
private String mediaId;
}
@Data
public static class VoiceContent {
private String mediaId;
}
@Data
public static class VideoContent {
private String mediaId;
}
@Data
public static class FileContent {
private String mediaId;
}
@Data
public static class LocationContent {
private String latitude;
private String longitude;
private String name;
private String address;
}
@Data
public static class LinkContent {
private String title;
private String desc;
private String url;
private String picUrl;
}
@Data
public static class BusinessCardContent {
private String userid;
}
@Data
public static class MiniprogramContent {
private String title;
private String appid;
private String pagepath;
private String thumbMediaId;
}
@Data
public static class EventContent {
private String eventType;
private String openKfid;
private String externalUserid;
private String scene;
private String sceneParam;
private String welcomeCode;
private String wechatChannels;
private String failMsgid;
private String failType;
private String servicerUserid;
private Integer status;
private String changeType;
private String oldServicerUserid;
private String newServicerUserid;
private String msgCode;
private String recallMsgid;
}
}

View File

@@ -0,0 +1,23 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @description 素材上传响应
* @filename MediaUploadResponse.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MediaUploadResponse extends WeChatResponse {
private String type;
private String mediaId;
private Long createdAt;
}

View File

@@ -0,0 +1,23 @@
package org.xyzh.common.wechat.pojo.kefu;
import lombok.Data;
/**
* @description 微信API通用响应
* @filename WeChatResponse.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Data
public class WeChatResponse {
private Integer errcode;
private String errmsg;
public boolean isSuccess() {
return errcode == null || errcode == 0;
}
}

View File

@@ -22,6 +22,7 @@
<module>common-jdbc</module>
<module>common-all</module>
<module>common-exception</module>
<module>common-wechat</module>
</modules>
<properties>
@@ -71,6 +72,11 @@
<artifactId>common-jdbc</artifactId>
<version>${urban-lifeline.version}</version>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-wechat</artifactId>
<version>${urban-lifeline.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>