客服模块

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

@@ -85,6 +85,14 @@ INSERT INTO config.tb_sys_config (
-- 平台特性
('CFG-0601', 'cfg_maintenance', 'platform.maintenance', '维护模式', 'false', 'BOOLEAN', 'switch', '维护模式开关', NULL, NULL, 'platform', 'mod_system', 10, 0, 'true时仅管理员可用', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0602', 'cfg_feature_acl_policy','feature.acl.policy', 'ACL策略', 'enabled', 'String', 'select', 'ACL策略开关', NULL, '["enabled", "disabled"]'::json, 'platform', 'mod_system', 20, 0, 'enabled/disabled', 'system', NULL, NULL, now(), NULL, NULL, false);
('CFG-0602', 'cfg_feature_acl_policy','feature.acl.policy', 'ACL策略', 'enabled', 'String', 'select', 'ACL策略开关', NULL, '["enabled", "disabled"]'::json, 'platform', 'mod_system', 20, 0, 'enabled/disabled', 'system', NULL, NULL, now(), NULL, NULL, false),
-- 微信客服配置
('CFG-0701', 'cfg_wechat_kefu_corpid', 'wechat.kefu.corpId', '企业ID', '', 'String', 'input', '企业微信的企业ID', NULL, NULL, 'wechat', 'mod_workcase', 10, 1, '企业微信管理后台获取', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0702', 'cfg_wechat_kefu_secret', 'wechat.kefu.secret', '客服应用Secret', '', 'String', 'password', '微信客服应用的Secret', NULL, NULL, 'wechat', 'mod_workcase', 20, 1, '微信客服应用的密钥', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0703', 'cfg_wechat_kefu_token', 'wechat.kefu.token', '回调Token', '', 'String', 'input', '消息回调的Token', NULL, NULL, 'wechat', 'mod_workcase', 30, 1, '用于验证消息回调的Token', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0704', 'cfg_wechat_kefu_aeskey', 'wechat.kefu.encodingAesKey','回调加密密钥', '', 'String', 'password', '消息回调的EncodingAESKey', NULL, NULL, 'wechat', 'mod_workcase', 40, 1, '用于解密消息回调的AES密钥', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0705', 'cfg_wechat_kefu_openkfid', 'wechat.kefu.openKfid', '客服账号ID', '', 'String', 'input', '微信客服账号的open_kfid', NULL, NULL, 'wechat', 'mod_workcase', 50, 1, '用于发送消息的客服账号ID', 'system', NULL, NULL, now(), NULL, NULL, false),
('CFG-0706', 'cfg_wechat_kefu_welcome', 'wechat.kefu.welcomeTemplate','欢迎语模板', '您好,您的工单已创建。\n工单编号{workcaseId}\n问题类型{type}\n设备{device}\n我们将尽快为您处理。', 'String', 'textarea', '客服欢迎语消息模板', NULL, NULL, 'wechat', 'mod_workcase', 60, 1, '支持变量:{workcaseId},{type},{device},{username}', 'system', NULL, NULL, now(), NULL, NULL, false);

View File

@@ -42,13 +42,6 @@ public interface WorkcaseChatService {
*/
ResultDomain<TbChat> getChatList(TbChat filter);
/**
* 获取聊天分页
* @param pageRequest
* @return 聊天分页
*/
ResultDomain<TbChat> getChatPage(PageRequest<TbChat> pageRequest);
// ========================= 聊天信息管理 ======================
/**

View File

@@ -6,7 +6,7 @@ import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
/**
* 工单服务接口
@@ -77,7 +77,7 @@ public interface WorkcaseService {
* @author yslg
* @since 2025-12-19
*/
ResultDomain<Void> receiveWorkcaseFromCrm(JSON json);
ResultDomain<Void> receiveWorkcaseFromCrm(JSONObject json);
// ====================== 工单处理过程 ======================
/**

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>

View File

@@ -300,6 +300,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>
<!-- 服务模块 -->
<dependency>
<groupId>org.xyzh</groupId>

View File

@@ -42,6 +42,10 @@
<groupId>org.xyzh.common</groupId>
<artifactId>common-exception</artifactId>
</dependency>
<dependency>
<groupId>org.xyzh.common</groupId>
<artifactId>common-wechat</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

View File

@@ -0,0 +1,32 @@
package org.xyzh.workcase.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.xyzh.common.wechat.kefu.message.KefuMessageService;
import org.xyzh.workcase.handler.WorkcaseKefuHandler;
import jakarta.annotation.PostConstruct;
/**
* @description 工单模块的微信客服配置
* 注册 WorkcaseKefuHandler 到消息服务
* @filename WeChatKefuConfig.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Configuration
public class WeChatKefuConfig {
@Autowired
private KefuMessageService kefuMessageService;
@Autowired
private WorkcaseKefuHandler workcaseKefuHandler;
@PostConstruct
public void init() {
kefuMessageService.setHandler(workcaseKefuHandler);
}
}

View File

@@ -0,0 +1,229 @@
package org.xyzh.workcase.controller;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.xyzh.api.ai.dto.ChatPrepareData;
import org.xyzh.api.ai.dto.TbChat;
import org.xyzh.api.ai.dto.TbChatMessage;
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
import org.xyzh.api.workcase.service.WorkcaseChatService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.utils.validation.ValidationResult;
import org.xyzh.common.utils.validation.ValidationUtils;
import org.xyzh.common.wechat.kefu.message.KefuMessageService;
import io.swagger.v3.oas.annotations.Operation;
import java.util.Arrays;
import io.swagger.v3.oas.annotations.tags.Tag;
/**
* @description 工单对话控制器
* - AI对话管理
* - 微信客服消息接收
* - 词云管理
* @filename WorkcaseChatController.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Tag(name = "工单对话")
@RestController
@RequestMapping("/workcase/chat")
public class WorkcaseChatContorller {
@DubboReference(version = "1.0.0", group = "workcase", check = false)
private WorkcaseChatService workcaseChatService;
@Autowired
private KefuMessageService kefuMessageService;
// ========================= AI对话管理 =========================
@Operation(summary = "创建对话")
@PostMapping
public ResultDomain<TbChat> createChat(@RequestBody TbChat chat) {
ValidationResult vr = ValidationUtils.validate(chat, Arrays.asList(
ValidationUtils.requiredString("userId", "用户ID")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseChatService.createChat(chat);
}
@Operation(summary = "更新对话")
@PutMapping
public ResultDomain<TbChat> updateChat(@RequestBody TbChat chat) {
ValidationResult vr = ValidationUtils.validate(chat, Arrays.asList(
ValidationUtils.requiredString("chatId", "对话ID")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseChatService.updateChat(chat);
}
@Operation(summary = "查询对话列表")
@PostMapping("/list")
public ResultDomain<TbChat> getChatList(@RequestBody TbChat filter) {
return workcaseChatService.getChatList(filter);
}
@Operation(summary = "获取对话消息列表")
@PostMapping("/message/list")
public ResultDomain<TbChatMessage> getChatMessageList(@RequestBody TbChat filter) {
ValidationResult vr = ValidationUtils.validate(filter, Arrays.asList(
ValidationUtils.requiredString("chatId", "对话ID")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseChatService.getChatMessageList(filter);
}
@Operation(summary = "准备对话会话")
@PostMapping("/prepare")
public ResultDomain<String> prepareChatMessageSession(@RequestBody ChatPrepareData prepareData) {
ValidationResult vr = ValidationUtils.validate(prepareData, Arrays.asList(
ValidationUtils.requiredString("chatId", "对话ID"),
ValidationUtils.requiredString("query", "用户问题")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseChatService.prepareChatMessageSession(prepareData);
}
@Operation(summary = "流式对话SSE")
@GetMapping(value = "/stream/{sessionId}", produces = "text/event-stream")
public SseEmitter streamChatMessage(@PathVariable String sessionId) {
return workcaseChatService.streamChatMessageWithSse(sessionId);
}
@Operation(summary = "停止对话")
@PostMapping("/stop/{taskId}")
public ResultDomain<Boolean> stopChat(@RequestBody TbChat filter, @PathVariable String taskId) {
return workcaseChatService.stopChatMessageByTaskId(filter, taskId);
}
@Operation(summary = "评论对话消息")
@PostMapping("/comment")
public ResultDomain<Boolean> commentChatMessage(@RequestBody TbChat filter,
@RequestParam String messageId, @RequestParam String comment) {
return workcaseChatService.commentChatMessage(filter, messageId, comment);
}
// ========================= 对话分析 =========================
@Operation(summary = "分析对话AI预填工单信息")
@GetMapping("/analyze/{chatId}")
public ResultDomain<TbWorkcaseDTO> analyzeChat(@PathVariable String chatId) {
return workcaseChatService.analyzeChat(chatId);
}
@Operation(summary = "总结对话")
@PostMapping("/summary/{chatId}")
public ResultDomain<TbWorkcaseDTO> summaryChat(@PathVariable String chatId) {
return workcaseChatService.summaryChat(chatId);
}
// ========================= 微信客服消息回调 =========================
@Operation(summary = "微信客服消息回调验证GET")
@GetMapping("/kefu/callback")
public String kefuCallbackVerify(
@RequestParam("msg_signature") String msgSignature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("echostr") String echostr) {
// TODO: 验证签名并返回 echostr
// 实际应使用微信提供的加解密工具验证
return echostr;
}
@Operation(summary = "微信客服消息回调POST")
@PostMapping("/kefu/callback")
public String kefuCallback(
@RequestParam("msg_signature") String msgSignature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestBody String xmlBody) {
// TODO: 解密消息,调用同步接口拉取消息
// 收到回调后,应调用 kefuMessageService.syncMessages() 拉取新消息
// 然后通过 processMessages() 处理消息
return "success";
}
@Operation(summary = "手动同步客服消息")
@PostMapping("/kefu/sync")
public ResultDomain<String> syncKefuMessages(
@RequestParam(required = false) String cursor,
@RequestParam(required = false) String token,
@RequestParam(required = false, defaultValue = "100") Integer limit) {
var response = kefuMessageService.syncMessages(cursor, token, limit);
if (response != null && response.isSuccess()) {
kefuMessageService.processMessages(response);
return ResultDomain.success("同步成功", response.getNextCursor());
}
return ResultDomain.failure("同步失败");
}
// ========================= 词云管理 =========================
@Operation(summary = "添加词云")
@PostMapping("/wordcloud")
public ResultDomain<TbWordCloudDTO> addWordCloud(@RequestBody TbWordCloudDTO wordCloud) {
ValidationResult vr = ValidationUtils.validate(wordCloud, Arrays.asList(
ValidationUtils.requiredString("word", "词语"),
ValidationUtils.requiredString("category", "分类")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseChatService.addWordCloud(wordCloud);
}
@Operation(summary = "更新词云")
@PutMapping("/wordcloud")
public ResultDomain<TbWordCloudDTO> updateWordCloud(@RequestBody TbWordCloudDTO wordCloud) {
ValidationResult vr = ValidationUtils.validate(wordCloud, Arrays.asList(
ValidationUtils.requiredString("wordCloudId", "词云ID")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseChatService.updateWordCloud(wordCloud);
}
@Operation(summary = "查询词云列表")
@PostMapping("/wordcloud/list")
public ResultDomain<TbWordCloudDTO> getWordCloudList(@RequestBody TbWordCloudDTO filter) {
return workcaseChatService.getWordCloudList(filter);
}
@Operation(summary = "分页查询词云")
@PostMapping("/wordcloud/page")
public ResultDomain<TbWordCloudDTO> getWordCloudPage(@RequestBody PageRequest<TbWordCloudDTO> pageRequest) {
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null),
ValidationUtils.requiredNumber("pageParam.pageSize", "每页数量", 1, 100)
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseChatService.getWordCloudPage(pageRequest);
}
}

View File

@@ -0,0 +1,233 @@
package org.xyzh.workcase.controller;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO;
import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO;
import org.xyzh.api.workcase.service.WorkcaseService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.utils.validation.ValidationResult;
import org.xyzh.common.utils.validation.ValidationUtils;
import com.alibaba.fastjson2.JSONObject;
import io.swagger.v3.oas.annotations.Operation;
import java.util.Arrays;
import io.swagger.v3.oas.annotations.tags.Tag;
/**
* @description 工单管理控制器
* @filename WorkcaseController.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Tag(name = "工单管理")
@RestController
@RequestMapping("/workcase")
public class WorkcaseController {
@DubboReference(version = "1.0.0", group = "workcase", check = false)
private WorkcaseService workcaseService;
// ========================= 工单管理 =========================
@Operation(summary = "创建工单")
@PostMapping
public ResultDomain<TbWorkcaseDTO> createWorkcase(@RequestBody TbWorkcaseDTO workcase) {
ValidationResult vr = ValidationUtils.validate(workcase, Arrays.asList(
ValidationUtils.requiredString("userId", "用户ID"),
ValidationUtils.requiredString("type", "问题类型")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseService.createWorkcase(workcase);
}
@Operation(summary = "更新工单")
@PutMapping
public ResultDomain<TbWorkcaseDTO> updateWorkcase(@RequestBody TbWorkcaseDTO workcase) {
ValidationResult vr = ValidationUtils.validate(workcase, Arrays.asList(
ValidationUtils.requiredString("workcaseId", "工单ID")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseService.updateWorkcase(workcase);
}
@Operation(summary = "删除工单")
@DeleteMapping("/{workcaseId}")
public ResultDomain<TbWorkcaseDTO> deleteWorkcase(@PathVariable String workcaseId) {
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
workcase.setWorkcaseId(workcaseId);
return workcaseService.deleteWorkcase(workcase);
}
@Operation(summary = "获取工单详情")
@GetMapping("/{workcaseId}")
public ResultDomain<TbWorkcaseDTO> getWorkcaseById(@PathVariable String workcaseId) {
return workcaseService.getWorkcaseById(workcaseId);
}
@Operation(summary = "查询工单列表")
@PostMapping("/list")
public ResultDomain<TbWorkcaseDTO> getWorkcaseList(@RequestBody TbWorkcaseDTO filter) {
return workcaseService.getWorkcaseList(filter);
}
@Operation(summary = "分页查询工单")
@PostMapping("/page")
public ResultDomain<TbWorkcaseDTO> getWorkcasePage(@RequestBody PageRequest<TbWorkcaseDTO> pageRequest) {
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null),
ValidationUtils.requiredNumber("pageParam.pageSize", "每页数量", 1, 100)
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseService.getWorkcasePage(pageRequest);
}
// ========================= CRM同步接口 =========================
@Operation(summary = "同步工单到CRM")
@PostMapping("/sync/crm")
public ResultDomain<Void> syncWorkcaseToCrm(@RequestBody TbWorkcaseDTO workcase) {
ValidationResult vr = ValidationUtils.validate(workcase, Arrays.asList(
ValidationUtils.requiredString("workcaseId", "工单ID")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseService.syncWorkcaseToCrm(workcase);
}
@Operation(summary = "接收CRM工单更新CRM回调")
@PostMapping("/receive/crm")
public ResultDomain<Void> receiveWorkcaseFromCrm(@RequestBody String jsonBody) {
JSONObject json = JSONObject.parseObject(jsonBody);
return workcaseService.receiveWorkcaseFromCrm(json);
}
// ========================= 工单处理过程 =========================
@Operation(summary = "创建工单处理过程")
@PostMapping("/process")
public ResultDomain<TbWorkcaseProcessDTO> createWorkcaseProcess(@RequestBody TbWorkcaseProcessDTO process) {
ValidationResult vr = ValidationUtils.validate(process, Arrays.asList(
ValidationUtils.requiredString("workcaseId", "工单ID"),
ValidationUtils.requiredString("action", "操作类型")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseService.createWorkcaseProcess(process);
}
@Operation(summary = "更新工单处理过程")
@PutMapping("/process")
public ResultDomain<TbWorkcaseProcessDTO> updateWorkcaseProcess(@RequestBody TbWorkcaseProcessDTO process) {
ValidationResult vr = ValidationUtils.validate(process, Arrays.asList(
ValidationUtils.requiredString("processId", "处理过程ID")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseService.updateWorkcaseProcess(process);
}
@Operation(summary = "删除工单处理过程")
@DeleteMapping("/process/{processId}")
public ResultDomain<TbWorkcaseProcessDTO> deleteWorkcaseProcess(@PathVariable String processId) {
TbWorkcaseProcessDTO process = new TbWorkcaseProcessDTO();
process.setProcessId(processId);
return workcaseService.deleteWorkcaseProcess(process);
}
@Operation(summary = "查询工单处理过程列表")
@PostMapping("/process/list")
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessList(@RequestBody TbWorkcaseProcessDTO filter) {
return workcaseService.getWorkcaseProcessList(filter);
}
@Operation(summary = "分页查询工单处理过程")
@PostMapping("/process/page")
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessPage(@RequestBody PageRequest<TbWorkcaseProcessDTO> pageRequest) {
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null),
ValidationUtils.requiredNumber("pageParam.pageSize", "每页数量", 1, 100)
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseService.getWorkcaseProcessPage(pageRequest);
}
// ========================= 工单设备管理 =========================
@Operation(summary = "创建工单设备")
@PostMapping("/device")
public ResultDomain<TbWorkcaseDeviceDTO> createWorkcaseDevice(@RequestBody TbWorkcaseDeviceDTO device) {
ValidationResult vr = ValidationUtils.validate(device, Arrays.asList(
ValidationUtils.requiredString("workcaseId", "工单ID"),
ValidationUtils.requiredString("device", "设备名称")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseService.createWorkcaseDevice(device);
}
@Operation(summary = "更新工单设备")
@PutMapping("/device")
public ResultDomain<TbWorkcaseDeviceDTO> updateWorkcaseDevice(@RequestBody TbWorkcaseDeviceDTO device) {
ValidationResult vr = ValidationUtils.validate(device, Arrays.asList(
ValidationUtils.requiredString("workcaseId", "工单ID"),
ValidationUtils.requiredString("device", "设备名称")
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseService.updateWorkcaseDevice(device);
}
@Operation(summary = "删除工单设备")
@DeleteMapping("/device/{workcaseId}/{device}")
public ResultDomain<TbWorkcaseDeviceDTO> deleteWorkcaseDevice(@PathVariable String workcaseId, @PathVariable String device) {
TbWorkcaseDeviceDTO deviceDTO = new TbWorkcaseDeviceDTO();
deviceDTO.setWorkcaseId(workcaseId);
deviceDTO.setDevice(device);
return workcaseService.deleteWorkcaseDevice(deviceDTO);
}
@Operation(summary = "查询工单设备列表")
@PostMapping("/device/list")
public ResultDomain<TbWorkcaseDeviceDTO> getWorkcaseDeviceList(@RequestBody TbWorkcaseDeviceDTO filter) {
return workcaseService.getWorkcaseDeviceList(filter);
}
@Operation(summary = "分页查询工单设备")
@PostMapping("/device/page")
public ResultDomain<TbWorkcaseDeviceDTO> getWorkcaseDevicePage(@RequestBody PageRequest<TbWorkcaseDeviceDTO> pageRequest) {
ValidationResult vr = ValidationUtils.validate(pageRequest, Arrays.asList(
ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null),
ValidationUtils.requiredNumber("pageParam.pageSize", "每页数量", 1, 100)
));
if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors());
}
return workcaseService.getWorkcaseDevicePage(pageRequest);
}
}

View File

@@ -0,0 +1,120 @@
package org.xyzh.workcase.handler;
import org.apache.dubbo.config.annotation.DubboReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.xyzh.api.ai.dto.TbChatMessage;
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
import org.xyzh.api.workcase.service.WorkcaseChatService;
import org.xyzh.api.workcase.service.WorkcaseService;
import org.xyzh.common.wechat.kefu.message.KefuMessageHandler;
import org.xyzh.common.wechat.kefu.message.KefuMessageService;
import org.xyzh.common.wechat.pojo.kefu.KefuSyncMsgResponse.KefuSyncMsg;
/**
* @description 工单模块的微信客服处理器实现
* 处理微信客服消息,同步到工单系统
* @filename WorkcaseKefuHandler.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Component
public class WorkcaseKefuHandler implements KefuMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(WorkcaseKefuHandler.class);
@Autowired
private KefuMessageService kefuMessageService;
@DubboReference(version = "1.0.0", group = "workcase", check = false)
private WorkcaseService workcaseService;
@DubboReference(version = "1.0.0", group = "workcase", check = false)
private WorkcaseChatService workcaseChatService;
@Override
public void onEnterSession(String openKfid, String externalUserid, String scene, String sceneParam, String welcomeCode) {
logger.info("工单客服-用户进入会话: externalUserid={}, scene={}, sceneParam={}", externalUserid, scene, sceneParam);
// sceneParam 可能包含 workcaseId用于关联工单
if (sceneParam != null && !sceneParam.isEmpty()) {
TbWorkcaseDTO filter = new TbWorkcaseDTO();
filter.setWorkcaseId(sceneParam);
var result = workcaseService.getWorkcaseById(sceneParam);
if (result != null && result.getSuccess() && result.getData() != null) {
TbWorkcaseDTO workcase = result.getData();
// 发送工单信息作为欢迎语
String welcomeMsg = buildWelcomeMessage(workcase);
kefuMessageService.sendWelcomeMessage(welcomeCode, welcomeMsg);
logger.info("已发送工单欢迎语: workcaseId={}", workcase.getWorkcaseId());
} else {
// 发送通用欢迎语
kefuMessageService.sendWelcomeMessage(welcomeCode, "您好,欢迎使用客服服务,请问有什么可以帮您?");
}
} else {
kefuMessageService.sendWelcomeMessage(welcomeCode, "您好,欢迎使用客服服务,请问有什么可以帮您?");
}
}
@Override
public void onTextMessage(String openKfid, String externalUserid, String msgid, String content, Long sendTime) {
logger.info("工单客服-收到文本消息: externalUserid={}, content={}", externalUserid, content);
// 同步消息到 tb_chat_message 表
// TODO: 根据 externalUserid 查找关联的 chatId然后保存消息
TbChatMessage chatMessage = new TbChatMessage();
chatMessage.setRole("user");
chatMessage.setContent(content);
// workcaseChatService.saveChatMessage(chatMessage);
}
@Override
public void onImageMessage(String openKfid, String externalUserid, String msgid, String mediaId, Long sendTime) {
logger.info("工单客服-收到图片消息: externalUserid={}, mediaId={}", externalUserid, mediaId);
// 下载图片并保存
// TODO: 实现图片消息处理
}
@Override
public void onSessionStatusChange(String openKfid, String externalUserid, String changeType,
String oldServicerUserid, String newServicerUserid) {
logger.info("工单客服-会话状态变更: externalUserid={}, changeType={}, newServicer={}",
externalUserid, changeType, newServicerUserid);
// 更新工单处理人
// TODO: 根据 externalUserid 查找关联工单,更新 processor
}
@Override
public void onMsgSendFail(String openKfid, String externalUserid, String failMsgid, String failType) {
logger.warn("工单客服-消息发送失败: externalUserid={}, failMsgid={}, failType={}",
externalUserid, failMsgid, failType);
// 记录失败日志,可能需要重试
}
@Override
public void onOtherMessage(KefuSyncMsg msg) {
logger.info("工单客服-收到其他消息: msgtype={}", msg.getMsgtype());
}
private String buildWelcomeMessage(TbWorkcaseDTO workcase) {
StringBuilder sb = new StringBuilder();
sb.append("您好,您的工单已创建。\n");
sb.append("工单编号:").append(workcase.getWorkcaseId()).append("\n");
if (workcase.getType() != null) {
sb.append("问题类型:").append(workcase.getType()).append("\n");
}
if (workcase.getDevice() != null) {
sb.append("设备:").append(workcase.getDevice()).append("\n");
}
sb.append("我们将尽快为您处理。");
return sb.toString();
}
}

View File

@@ -0,0 +1,60 @@
package org.xyzh.workcase.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
import org.xyzh.common.core.page.PageParam;
/**
* @description 词云数据访问层
* @filename TbWordCloudMapper.java
* @author yslg
* @copyright xyzh
* @since 2025-12-19
*/
@Mapper
public interface TbWordCloudMapper {
/**
* 插入词云
*/
int insertWordCloud(TbWordCloudDTO wordCloud);
/**
* 更新词云只更新非null字段
*/
int updateWordCloud(TbWordCloudDTO wordCloud);
/**
* 根据ID查询词云
*/
TbWordCloudDTO selectWordCloudById(@Param("wordId") String wordId);
/**
* 查询单条词云(用于更新词频等场景)
*/
TbWordCloudDTO selectWordCloudOne(@Param("filter") TbWordCloudDTO filter);
/**
* 查询词云列表
*/
List<TbWordCloudDTO> selectWordCloudList(@Param("filter") TbWordCloudDTO filter);
/**
* 分页查询词云
*/
List<TbWordCloudDTO> selectWordCloudPage(@Param("filter") TbWordCloudDTO filter, @Param("pageParam") PageParam pageParam);
/**
* 统计词云数量
*/
long countWordClouds(@Param("filter") TbWordCloudDTO filter);
/**
* 增加词频
*/
int incrementFrequency(@Param("wordId") String wordId, @Param("count") int count);
}

View File

@@ -1,113 +1,294 @@
package org.xyzh.workcase.service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.xyzh.api.ai.dto.ChatPrepareData;
import org.xyzh.api.ai.dto.TbChat;
import org.xyzh.api.ai.dto.TbChatMessage;
import org.xyzh.api.ai.service.AgentChatService;
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
import org.xyzh.api.workcase.service.WorkcaseChatService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.redis.service.RedisService;
import org.xyzh.common.utils.id.IdUtil;
import org.xyzh.workcase.mapper.TbWordCloudMapper;
@DubboService(version = "1.0.0",group = "workcase",timeout = 30000,retries = 0)
public class WorkcaseChatServiceImpl implements WorkcaseChatService{
private static final Logger logger = LoggerFactory.getLogger(WorkcaseChatServiceImpl.class);
@Override
public ResultDomain<TbWordCloudDTO> addWordCloud(TbWordCloudDTO wordCloud) {
// TODO Auto-generated method stub
return null;
}
private static final String CHAT_COUNT_KEY_PREFIX = "workcase:chat:count:";
private static final int TRANSFER_HUMAN_THRESHOLD = 3;
@Override
public ResultDomain<TbWorkcaseDTO> analyzeChat(String chatId) {
// TODO Auto-generated method stub
return null;
}
@DubboReference(version = "1.0.0", group = "ai", check = false)
private AgentChatService agentChatService;
@Override
public ResultDomain<Boolean> commentChatMessage(TbChat filter, String messageId, String comment) {
// TODO Auto-generated method stub
return null;
}
@Autowired
private TbWordCloudMapper wordCloudMapper;
@Autowired
private RedisService redisService;
// ========================= 聊天管理 ==========================
@Override
public ResultDomain<TbChat> createChat(TbChat chat) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbChat> getChatList(TbChat filter) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbChatMessage> getChatMessageList(TbChat filter) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbChat> getChatPage(PageRequest<TbChat> pageRequest) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWordCloudDTO> getWordCloudList(TbWordCloudDTO filter) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWordCloudDTO> getWordCloudPage(PageRequest<TbWordCloudDTO> pageRequest) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<String> prepareChatMessageSession(ChatPrepareData prepareData) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<Boolean> stopChatMessageByTaskId(TbChat filter, String taskId) {
// TODO Auto-generated method stub
return null;
}
@Override
public SseEmitter streamChatMessageWithSse(String sessionId) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseDTO> summaryChat(String chatId) {
// TODO Auto-generated method stub
return null;
logger.info("创建对话: userId={}", chat.getUserId());
ResultDomain<TbChat> result = agentChatService.createChat(chat);
if (result.getSuccess() && result.getData() != null) {
redisService.set(CHAT_COUNT_KEY_PREFIX + result.getData().getChatId(), 0);
}
return result;
}
@Override
public ResultDomain<TbChat> updateChat(TbChat chat) {
// TODO Auto-generated method stub
return null;
logger.info("更新对话: chatId={}", chat.getChatId());
return agentChatService.updateChat(chat);
}
@Override
public ResultDomain<TbChat> getChatList(TbChat filter) {
return agentChatService.getChatList(filter);
}
// ========================= 聊天信息管理 ======================
@Override
public ResultDomain<TbChatMessage> getChatMessageList(TbChat filter) {
return agentChatService.getChatMessageList(filter);
}
@Override
public ResultDomain<String> prepareChatMessageSession(ChatPrepareData prepareData) {
logger.info("准备对话会话: chatId={}, query={}", prepareData.getChatId(), prepareData.getQuery());
String chatId = prepareData.getChatId();
Object countObj = redisService.get(CHAT_COUNT_KEY_PREFIX + chatId);
int chatCount = (countObj != null) ? (Integer) countObj : 0;
chatCount++;
redisService.set(CHAT_COUNT_KEY_PREFIX + chatId, chatCount);
ResultDomain<String> result = agentChatService.prepareChatMessageSession(prepareData);
if (result.getSuccess() && chatCount >= TRANSFER_HUMAN_THRESHOLD) {
logger.info("已达到{}次AI对话建议转人工: chatId={}", TRANSFER_HUMAN_THRESHOLD, chatId);
}
return result;
}
@Override
public SseEmitter streamChatMessageWithSse(String sessionId) {
return agentChatService.streamChatMessageWithSse(sessionId);
}
@Override
public ResultDomain<Boolean> stopChatMessageByTaskId(TbChat filter, String taskId) {
return agentChatService.stopChatMessageByTaskId(filter, taskId);
}
@Override
public ResultDomain<Boolean> commentChatMessage(TbChat filter, String messageId, String comment) {
return agentChatService.commentChatMessage(filter, messageId, comment);
}
// =============================== 对话分析 ==========================
@Override
public ResultDomain<TbWorkcaseDTO> analyzeChat(String chatId) {
logger.info("分析对话内容,生成工单预填信息: chatId={}", chatId);
TbChat filter = new TbChat();
filter.setChatId(chatId);
ResultDomain<TbChatMessage> msgResult = agentChatService.getChatMessageList(filter);
if (!msgResult.getSuccess() || msgResult.getDataList() == null || msgResult.getDataList().isEmpty()) {
return ResultDomain.failure("获取对话消息失败或消息为空");
}
List<TbChatMessage> messages = msgResult.getDataList();
// ============== 伪代码调用AI分析对话自动生成工单预填信息 ==============
// 步骤5AI根据聊天对话自动生成部分工单信息预填入小程序的工单创建表单
//
// 1. 构建对话上下文
// StringBuilder conversationContext = new StringBuilder();
// for (TbChatMessage msg : messages) {
// conversationContext.append(msg.getRole()).append(": ").append(msg.getContent()).append("\n");
// }
//
// 2. 调用Dify工作流或Agent进行对话分析
// DifyWorkflowRequest request = new DifyWorkflowRequest();
// request.setWorkflowId("workcase-analysis-workflow");
// request.setInputs(Map.of("conversation", conversationContext.toString()));
// DifyWorkflowResponse response = difyService.runWorkflow(request);
//
// 3. 解析AI分析结果提取工单预填信息
// JSONObject analysisResult = JSON.parseObject(response.getOutputs());
// TbWorkcaseDTO workcase = new TbWorkcaseDTO();
// workcase.setType(analysisResult.getString("type")); // 问题类型:如"设备故障"、"维修申请"
// workcase.setDevice(analysisResult.getString("device")); // 设备名称:如"燃气管道"、"电梯"
// workcase.setDeviceCode(analysisResult.getString("deviceCode")); // 设备编号(如果用户提到)
// workcase.setEmergency(analysisResult.getString("emergency")); // 紧急程度normal/urgent/critical
// workcase.setRemark(analysisResult.getString("description")); // 问题描述摘要
// ============== 伪代码结束 ==============
// 模拟AI分析结果实际应由AI返回
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
workcase.setType("设备故障");
workcase.setDevice("待用户确认");
workcase.setDeviceCode("");
workcase.setEmergency("normal");
workcase.setRemark("对话ID:" + chatId + " | " + buildConversationSummary(messages));
logger.info("对话分析完成,工单预填信息已生成: chatId={}", chatId);
return ResultDomain.success("对话分析完成", workcase);
}
private String buildConversationSummary(List<TbChatMessage> messages) {
StringBuilder summary = new StringBuilder();
for (TbChatMessage msg : messages) {
if ("user".equals(msg.getRole())) {
summary.append(msg.getContent()).append(" ");
}
}
String result = summary.toString().trim();
return result.length() > 200 ? result.substring(0, 200) + "..." : result;
}
@Override
public ResultDomain<TbWorkcaseDTO> summaryChat(String chatId) {
logger.info("总结对话: chatId={}", chatId);
TbChat filter = new TbChat();
filter.setChatId(chatId);
ResultDomain<TbChatMessage> msgResult = agentChatService.getChatMessageList(filter);
if (!msgResult.getSuccess() || msgResult.getDataList() == null) {
return ResultDomain.failure("获取对话消息失败");
}
List<TbChatMessage> messages = msgResult.getDataList();
// TODO: 调用AI进行对话总结提取关键词更新词云
// 伪代码:
// String summary = aiService.summarize(messages);
// List<String> keywords = aiService.extractKeywords(messages);
// updateWordCloud(keywords);
extractAndUpdateWordCloud(messages);
TbWorkcaseDTO summary = new TbWorkcaseDTO();
logger.info("对话总结完成: chatId={}", chatId);
return ResultDomain.success("对话总结完成", summary);
}
private void extractAndUpdateWordCloud(List<TbChatMessage> messages) {
// TODO: 调用AI提取关键词
// 伪代码List<String> keywords = aiService.extractKeywords(messages);
// 模拟提取的关键词
String[] mockKeywords = {"故障", "维修", "设备"};
String today = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
for (String keyword : mockKeywords) {
TbWordCloudDTO queryFilter = new TbWordCloudDTO();
queryFilter.setWord(keyword);
queryFilter.setCategory("fault");
queryFilter.setStatDate(today);
TbWordCloudDTO existing = wordCloudMapper.selectWordCloudOne(queryFilter);
if (existing != null) {
wordCloudMapper.incrementFrequency(existing.getWordId(), 1);
} else {
TbWordCloudDTO wordCloud = new TbWordCloudDTO();
wordCloud.setWordId(IdUtil.generateUUID());
wordCloud.setWord(keyword);
wordCloud.setCategory("fault");
wordCloud.setStatDate(today);
wordCloud.setFrequency("1");
wordCloudMapper.insertWordCloud(wordCloud);
}
}
}
// =============================== 词云管理 ==========================
@Override
public ResultDomain<TbWordCloudDTO> addWordCloud(TbWordCloudDTO wordCloud) {
logger.info("添加词云: word={}", wordCloud.getWord());
if (wordCloud.getWordId() == null || wordCloud.getWordId().isEmpty()) {
wordCloud.setWordId(IdUtil.generateUUID());
}
if (wordCloud.getStatDate() == null || wordCloud.getStatDate().isEmpty()) {
wordCloud.setStatDate(LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
}
if (wordCloud.getFrequency() == null) {
wordCloud.setFrequency("1");
}
TbWordCloudDTO queryFilter = new TbWordCloudDTO();
queryFilter.setWord(wordCloud.getWord());
queryFilter.setCategory(wordCloud.getCategory());
queryFilter.setStatDate(wordCloud.getStatDate());
TbWordCloudDTO existing = wordCloudMapper.selectWordCloudOne(queryFilter);
if (existing != null) {
wordCloudMapper.incrementFrequency(existing.getWordId(), Integer.parseInt(wordCloud.getFrequency()));
TbWordCloudDTO updated = wordCloudMapper.selectWordCloudById(existing.getWordId());
return ResultDomain.success("词频更新成功", updated);
}
int rows = wordCloudMapper.insertWordCloud(wordCloud);
if (rows > 0) {
return ResultDomain.success("添加成功", wordCloud);
}
return ResultDomain.failure("添加失败");
}
@Override
public ResultDomain<TbWordCloudDTO> updateWordCloud(TbWordCloudDTO wordCloud) {
// TODO Auto-generated method stub
return null;
logger.info("更新词云: wordId={}", wordCloud.getWordId());
int rows = wordCloudMapper.updateWordCloud(wordCloud);
if (rows > 0) {
TbWordCloudDTO updated = wordCloudMapper.selectWordCloudById(wordCloud.getWordId());
return ResultDomain.success("更新成功", updated);
}
return ResultDomain.failure("更新失败");
}
@Override
public ResultDomain<TbWordCloudDTO> getWordCloudList(TbWordCloudDTO filter) {
List<TbWordCloudDTO> list = wordCloudMapper.selectWordCloudList(filter);
return ResultDomain.success("查询成功", list);
}
@Override
public ResultDomain<TbWordCloudDTO> getWordCloudPage(PageRequest<TbWordCloudDTO> pageRequest) {
TbWordCloudDTO filter = pageRequest.getFilter();
PageParam pageParam = pageRequest.getPageParam();
List<TbWordCloudDTO> list = wordCloudMapper.selectWordCloudPage(filter, pageParam);
long total = wordCloudMapper.countWordClouds(filter);
pageParam.setTotal((int) total);
PageDomain<TbWordCloudDTO> pageDomain = new PageDomain<>(pageParam, list);
return ResultDomain.success("查询成功", pageDomain);
}
}

View File

@@ -1,128 +1,435 @@
package org.xyzh.workcase.service;
import java.util.List;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.xyzh.api.workcase.dto.TbWorkcaseDTO;
import org.xyzh.api.workcase.dto.TbWorkcaseDeviceDTO;
import org.xyzh.api.workcase.dto.TbWorkcaseProcessDTO;
import org.xyzh.api.workcase.service.WorkcaseChatService;
import org.xyzh.api.workcase.service.WorkcaseService;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.core.page.PageParam;
import org.xyzh.common.core.page.PageRequest;
import org.xyzh.common.utils.id.IdUtil;
import org.xyzh.workcase.enums.WorkcaseProcessAction;
import org.xyzh.workcase.mapper.TbWorkcaseDeviceMapper;
import org.xyzh.workcase.mapper.TbWorkcaseMapper;
import org.xyzh.workcase.mapper.TbWorkcaseProcessMapper;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
@DubboService(version = "1.0.0",group = "workcase",timeout = 30000,retries = 0)
public class WorkcaseServiceImpl implements WorkcaseService {
private static final Logger logger = LoggerFactory.getLogger(WorkcaseServiceImpl.class);
@Autowired
private TbWorkcaseMapper workcaseMapper;
@Autowired
private TbWorkcaseProcessMapper workcaseProcessMapper;
@Autowired
private TbWorkcaseDeviceMapper workcaseDeviceMapper;
@DubboReference(version = "1.0.0", group = "workcase", check = false)
private WorkcaseChatService workcaseChatService;
// ====================== 工单管理 ======================
@Override
public ResultDomain<TbWorkcaseDTO> createWorkcase(TbWorkcaseDTO workcase) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseDeviceDTO> createWorkcaseDevice(TbWorkcaseDeviceDTO workcaseDevice) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseProcessDTO> createWorkcaseProcess(TbWorkcaseProcessDTO workcaseProcess) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseDTO> deleteWorkcase(TbWorkcaseDTO workcase) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseDeviceDTO> deleteWorkcaseDevice(TbWorkcaseDeviceDTO workcaseDevice) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseProcessDTO> deleteWorkcaseProcess(TbWorkcaseProcessDTO workcaseProcess) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseDTO> getWorkcaseById(String workcaseId) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseDeviceDTO> getWorkcaseDeviceList(TbWorkcaseDeviceDTO filter) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseDeviceDTO> getWorkcaseDevicePage(PageRequest<TbWorkcaseDeviceDTO> pageRequest) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseDTO> getWorkcaseList(TbWorkcaseDTO filter) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseDTO> getWorkcasePage(PageRequest<TbWorkcaseDTO> pageRequest) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessList(TbWorkcaseProcessDTO filter) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessPage(PageRequest<TbWorkcaseProcessDTO> pageRequest) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<Void> receiveWorkcaseFromCrm(JSON json) {
// TODO Auto-generated method stub
return null;
}
@Override
public ResultDomain<Void> syncWorkcaseToCrm(TbWorkcaseDTO workcase) {
// TODO Auto-generated method stub
return null;
logger.info("创建工单: userId={}, type={}", workcase.getUserId(), workcase.getType());
if (workcase.getWorkcaseId() == null || workcase.getWorkcaseId().isEmpty()) {
workcase.setWorkcaseId(IdUtil.generateUUID());
}
if (workcase.getOptsn() == null || workcase.getOptsn().isEmpty()) {
workcase.setOptsn(IdUtil.getOptsn());
}
if (workcase.getStatus() == null || workcase.getStatus().isEmpty()) {
workcase.setStatus("pending");
}
if (workcase.getEmergency() == null || workcase.getEmergency().isEmpty()) {
workcase.setEmergency("normal");
}
int rows = workcaseMapper.insertWorkcase(workcase);
if (rows > 0) {
TbWorkcaseProcessDTO process = new TbWorkcaseProcessDTO();
process.setProcessId(IdUtil.generateUUID());
process.setOptsn(IdUtil.getOptsn());
process.setWorkcaseId(workcase.getWorkcaseId());
process.setAction(WorkcaseProcessAction.INFO.getName());
process.setMessage("工单创建");
process.setCreator(workcase.getCreator());
workcaseProcessMapper.insertWorkcaseProcess(process);
syncWorkcaseToCrm(workcase);
sendWechatKefuWelcome(workcase);
return ResultDomain.success("创建成功", workcase);
}
return ResultDomain.failure("创建失败");
}
@Override
public ResultDomain<TbWorkcaseDTO> updateWorkcase(TbWorkcaseDTO workcase) {
// TODO Auto-generated method stub
return null;
logger.info("更新工单: workcaseId={}, status={}", workcase.getWorkcaseId(), workcase.getStatus());
TbWorkcaseDTO existing = workcaseMapper.selectWorkcaseById(workcase.getWorkcaseId());
if (existing == null) {
return ResultDomain.failure("工单不存在");
}
String oldStatus = existing.getStatus();
int rows = workcaseMapper.updateWorkcase(workcase);
if (rows > 0) {
TbWorkcaseDTO updated = workcaseMapper.selectWorkcaseById(workcase.getWorkcaseId());
if (workcase.getStatus() != null && !workcase.getStatus().equals(oldStatus)) {
TbWorkcaseProcessDTO process = new TbWorkcaseProcessDTO();
process.setProcessId(IdUtil.generateUUID());
process.setOptsn(IdUtil.getOptsn());
process.setWorkcaseId(workcase.getWorkcaseId());
process.setCreator(workcase.getCreator());
if ("done".equals(workcase.getStatus())) {
process.setAction(WorkcaseProcessAction.FINISH.getName());
process.setMessage("工单完成");
workcaseChatService.summaryChat(existing.getWorkcaseId());
} else if ("cancelled".equals(workcase.getStatus())) {
process.setAction(WorkcaseProcessAction.REPEAL.getName());
process.setMessage("工单撤销");
workcaseChatService.summaryChat(existing.getWorkcaseId());
} else {
process.setAction(WorkcaseProcessAction.INFO.getName());
process.setMessage("状态变更: " + oldStatus + " -> " + workcase.getStatus());
}
workcaseProcessMapper.insertWorkcaseProcess(process);
}
syncWorkcaseToCrm(updated);
return ResultDomain.success("更新成功", updated);
}
return ResultDomain.failure("更新失败");
}
@Override
public ResultDomain<TbWorkcaseDeviceDTO> updateWorkcaseDevice(TbWorkcaseDeviceDTO workcaseDevice) {
// TODO Auto-generated method stub
return null;
public ResultDomain<TbWorkcaseDTO> deleteWorkcase(TbWorkcaseDTO workcase) {
logger.info("删除工单: workcaseId={}", workcase.getWorkcaseId());
int rows = workcaseMapper.deleteWorkcase(workcase);
if (rows > 0) {
return ResultDomain.success("删除成功", workcase);
}
return ResultDomain.failure("删除失败");
}
@Override
public ResultDomain<TbWorkcaseDTO> getWorkcaseById(String workcaseId) {
TbWorkcaseDTO workcase = workcaseMapper.selectWorkcaseById(workcaseId);
if (workcase != null) {
return ResultDomain.success("查询成功", workcase);
}
return ResultDomain.failure("工单不存在");
}
@Override
public ResultDomain<TbWorkcaseDTO> getWorkcaseList(TbWorkcaseDTO filter) {
List<TbWorkcaseDTO> list = workcaseMapper.selectWorkcaseList(filter);
return ResultDomain.success("查询成功", list);
}
@Override
public ResultDomain<TbWorkcaseDTO> getWorkcasePage(PageRequest<TbWorkcaseDTO> pageRequest) {
TbWorkcaseDTO filter = pageRequest.getFilter();
PageParam pageParam = pageRequest.getPageParam();
List<TbWorkcaseDTO> list = workcaseMapper.selectWorkcasePage(filter, pageParam);
long total = workcaseMapper.countWorkcases(filter);
pageParam.setTotal((int) total);
PageDomain<TbWorkcaseDTO> pageDomain = new PageDomain<>(pageParam, list);
return ResultDomain.success("查询成功", pageDomain);
}
// ====================== 同步到CRM和接收 ===================
@Override
public ResultDomain<Void> syncWorkcaseToCrm(TbWorkcaseDTO workcase) {
logger.info("同步工单到CRM: workcaseId={}", workcase.getWorkcaseId());
// ============== 伪代码同步工单到CRM系统 ==============
// 1. 构建CRM请求数据
// JSONObject crmData = new JSONObject();
// crmData.put("workcaseId", workcase.getWorkcaseId());
// crmData.put("userId", workcase.getUserId());
// crmData.put("username", workcase.getUsername());
// crmData.put("phone", workcase.getPhone());
// crmData.put("type", workcase.getType());
// crmData.put("device", workcase.getDevice());
// crmData.put("deviceCode", workcase.getDeviceCode());
// crmData.put("emergency", workcase.getEmergency());
// crmData.put("status", workcase.getStatus());
// crmData.put("createTime", workcase.getCreateTime());
// 2. 调用CRM接口
// String crmApiUrl = "https://crm.example.com/api/workcase/sync";
// HttpHeaders headers = new HttpHeaders();
// headers.setContentType(MediaType.APPLICATION_JSON);
// headers.set("Authorization", "Bearer " + crmToken);
// HttpEntity<String> request = new HttpEntity<>(crmData.toJSONString(), headers);
// ResponseEntity<String> response = restTemplate.postForEntity(crmApiUrl, request, String.class);
// 3. 处理响应
// if (response.getStatusCode() == HttpStatus.OK) {
// JSONObject result = JSON.parseObject(response.getBody());
// String crmWorkcaseId = result.getString("crmWorkcaseId");
// logger.info("同步成功CRM工单ID: {}", crmWorkcaseId);
// } else {
// logger.error("同步失败: {}", response.getBody());
// return ResultDomain.failure("同步CRM失败");
// }
// ============== 伪代码结束 ==============
logger.info("CRM同步完成伪代码: workcaseId={}", workcase.getWorkcaseId());
return ResultDomain.success("同步成功");
}
@Override
public ResultDomain<Void> receiveWorkcaseFromCrm(JSONObject json) {
logger.info("接收CRM工单更新");
// ============== 伪代码接收CRM的工单处理结果 ==============
// 1. 解析CRM推送的数据
// JSONObject crmData = (JSONObject) json;
// String workcaseId = crmData.getString("workcaseId");
// String status = crmData.getString("status");
// String processor = crmData.getString("processor");
// String message = crmData.getString("message");
// 2. 更新本地工单
// TbWorkcaseDTO workcase = new TbWorkcaseDTO();
// workcase.setWorkcaseId(workcaseId);
// workcase.setStatus(status);
// workcase.setProcessor(processor);
// workcaseMapper.updateWorkcase(workcase);
// 3. 记录处理过程
// TbWorkcaseProcessDTO process = new TbWorkcaseProcessDTO();
// process.setProcessId(UUID.randomUUID().toString().replace("-", ""));
// process.setOptsn(SnowflakeIdUtil.nextIdStr());
// process.setWorkcaseId(workcaseId);
// process.setAction(WorkcaseProcessAction.INFO.getName());
// process.setMessage("CRM更新: " + message);
// process.setProcessor(processor);
// process.setCreator("CRM_SYSTEM");
// workcaseProcessMapper.insertWorkcaseProcess(process);
// 4. 如果工单完成或撤销,触发总结
// if ("done".equals(status) || "cancelled".equals(status)) {
// workcaseChatService.summaryChat(workcaseId);
// }
// ============== 伪代码结束 ==============
logger.info("CRM工单接收处理完成伪代码");
return ResultDomain.success("接收成功");
}
private void sendWechatKefuWelcome(TbWorkcaseDTO workcase) {
logger.info("发送微信客服欢迎语: workcaseId={}", workcase.getWorkcaseId());
// ============== 伪代码:发送微信客服欢迎语 ==============
// 1. 构建欢迎语内容(包含工单基本信息)
// StringBuilder welcomeMsg = new StringBuilder();
// welcomeMsg.append("【工单信息】\n");
// welcomeMsg.append("工单编号:").append(workcase.getOptsn()).append("\n");
// welcomeMsg.append("故障类型:").append(workcase.getType()).append("\n");
// welcomeMsg.append("设备名称:").append(workcase.getDevice()).append("\n");
// welcomeMsg.append("紧急程度:").append("emergency".equals(workcase.getEmergency()) ? "紧急" : "普通").append("\n");
// welcomeMsg.append("联系人:").append(workcase.getUsername()).append("\n");
// welcomeMsg.append("联系电话:").append(workcase.getPhone()).append("\n");
// welcomeMsg.append("\n客服将尽快为您处理请稍候...");
// 2. 获取微信客服接入链接
// String kefuUrl = wechatKefuService.getKefuUrl(workcase.getUserId());
// 3. 调用微信客服API发送欢迎语
// JSONObject msgBody = new JSONObject();
// msgBody.put("touser", workcase.getUserId()); // 微信openid
// msgBody.put("msgtype", "text");
// JSONObject textContent = new JSONObject();
// textContent.put("content", welcomeMsg.toString());
// msgBody.put("text", textContent);
// String accessToken = wechatService.getAccessToken();
// String apiUrl = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" + accessToken;
// restTemplate.postForEntity(apiUrl, msgBody.toJSONString(), String.class);
// 4. 记录客服会话开始
// wechatKefuService.startSession(workcase.getUserId(), workcase.getWorkcaseId());
// ============== 伪代码结束 ==============
logger.info("微信客服欢迎语发送完成(伪代码): workcaseId={}", workcase.getWorkcaseId());
}
// ====================== 工单处理过程 ======================
@Override
public ResultDomain<TbWorkcaseProcessDTO> createWorkcaseProcess(TbWorkcaseProcessDTO workcaseProcess) {
logger.info("创建工单过程: workcaseId={}, action={}", workcaseProcess.getWorkcaseId(), workcaseProcess.getAction());
if (workcaseProcess.getProcessId() == null || workcaseProcess.getProcessId().isEmpty()) {
workcaseProcess.setProcessId(IdUtil.generateUUID());
}
if (workcaseProcess.getOptsn() == null || workcaseProcess.getOptsn().isEmpty()) {
workcaseProcess.setOptsn(IdUtil.getOptsn());
}
String action = workcaseProcess.getAction();
if (WorkcaseProcessAction.ASSIGN.getName().equals(action) ||
WorkcaseProcessAction.REDEPLOY.getName().equals(action)) {
if (workcaseProcess.getProcessor() != null) {
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
workcase.setWorkcaseId(workcaseProcess.getWorkcaseId());
workcase.setProcessor(workcaseProcess.getProcessor());
workcase.setStatus("processing");
workcaseMapper.updateWorkcase(workcase);
}
} else if (WorkcaseProcessAction.FINISH.getName().equals(action)) {
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
workcase.setWorkcaseId(workcaseProcess.getWorkcaseId());
workcase.setStatus("done");
workcaseMapper.updateWorkcase(workcase);
workcaseChatService.summaryChat(workcaseProcess.getWorkcaseId());
} else if (WorkcaseProcessAction.REPEAL.getName().equals(action)) {
TbWorkcaseDTO workcase = new TbWorkcaseDTO();
workcase.setWorkcaseId(workcaseProcess.getWorkcaseId());
workcase.setStatus("cancelled");
workcaseMapper.updateWorkcase(workcase);
workcaseChatService.summaryChat(workcaseProcess.getWorkcaseId());
}
int rows = workcaseProcessMapper.insertWorkcaseProcess(workcaseProcess);
if (rows > 0) {
TbWorkcaseDTO workcase = workcaseMapper.selectWorkcaseById(workcaseProcess.getWorkcaseId());
if (workcase != null) {
syncWorkcaseToCrm(workcase);
}
return ResultDomain.success("创建成功", workcaseProcess);
}
return ResultDomain.failure("创建失败");
}
@Override
public ResultDomain<TbWorkcaseProcessDTO> updateWorkcaseProcess(TbWorkcaseProcessDTO workcaseProcess) {
// TODO Auto-generated method stub
return null;
logger.info("更新工单过程: processId={}", workcaseProcess.getProcessId());
int rows = workcaseProcessMapper.updateWorkcaseProcess(workcaseProcess);
if (rows > 0) {
TbWorkcaseProcessDTO updated = workcaseProcessMapper.selectWorkcaseProcessById(workcaseProcess.getProcessId());
return ResultDomain.success("更新成功", updated);
}
return ResultDomain.failure("更新失败");
}
@Override
public ResultDomain<TbWorkcaseProcessDTO> deleteWorkcaseProcess(TbWorkcaseProcessDTO workcaseProcess) {
logger.info("删除工单过程: processId={}", workcaseProcess.getProcessId());
int rows = workcaseProcessMapper.deleteWorkcaseProcess(workcaseProcess.getProcessId());
if (rows > 0) {
return ResultDomain.success("删除成功", workcaseProcess);
}
return ResultDomain.failure("删除失败");
}
@Override
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessList(TbWorkcaseProcessDTO filter) {
List<TbWorkcaseProcessDTO> list = workcaseProcessMapper.selectWorkcaseProcessList(filter);
return ResultDomain.success("查询成功", list);
}
@Override
public ResultDomain<TbWorkcaseProcessDTO> getWorkcaseProcessPage(PageRequest<TbWorkcaseProcessDTO> pageRequest) {
TbWorkcaseProcessDTO filter = pageRequest.getFilter();
PageParam pageParam = pageRequest.getPageParam();
List<TbWorkcaseProcessDTO> list = workcaseProcessMapper.selectWorkcaseProcessPage(filter, pageParam);
long total = workcaseProcessMapper.countWorkcaseProcesses(filter);
pageParam.setTotal((int) total);
PageDomain<TbWorkcaseProcessDTO> pageDomain = new PageDomain<>(pageParam, list);
return ResultDomain.success("查询成功", pageDomain);
}
// ====================== 工单设备管理 ======================
@Override
public ResultDomain<TbWorkcaseDeviceDTO> createWorkcaseDevice(TbWorkcaseDeviceDTO workcaseDevice) {
logger.info("创建工单设备: workcaseId={}, device={}", workcaseDevice.getWorkcaseId(), workcaseDevice.getDevice());
if (workcaseDevice.getOptsn() == null || workcaseDevice.getOptsn().isEmpty()) {
workcaseDevice.setOptsn(IdUtil.getOptsn());
}
int rows = workcaseDeviceMapper.insertWorkcaseDevice(workcaseDevice);
if (rows > 0) {
return ResultDomain.success("创建成功", workcaseDevice);
}
return ResultDomain.failure("创建失败");
}
@Override
public ResultDomain<TbWorkcaseDeviceDTO> updateWorkcaseDevice(TbWorkcaseDeviceDTO workcaseDevice) {
logger.info("更新工单设备: workcaseId={}, fileId={}", workcaseDevice.getWorkcaseId(), workcaseDevice.getFileId());
int rows = workcaseDeviceMapper.updateWorkcaseDevice(workcaseDevice);
if (rows > 0) {
TbWorkcaseDeviceDTO updated = workcaseDeviceMapper.selectWorkcaseDeviceById(
workcaseDevice.getWorkcaseId(), workcaseDevice.getFileId());
return ResultDomain.success("更新成功", updated);
}
return ResultDomain.failure("更新失败");
}
@Override
public ResultDomain<TbWorkcaseDeviceDTO> deleteWorkcaseDevice(TbWorkcaseDeviceDTO workcaseDevice) {
logger.info("删除工单设备: workcaseId={}, fileId={}", workcaseDevice.getWorkcaseId(), workcaseDevice.getFileId());
int rows = workcaseDeviceMapper.deleteWorkcaseDevice(workcaseDevice.getWorkcaseId(), workcaseDevice.getFileId());
if (rows > 0) {
return ResultDomain.success("删除成功", workcaseDevice);
}
return ResultDomain.failure("删除失败");
}
@Override
public ResultDomain<TbWorkcaseDeviceDTO> getWorkcaseDeviceList(TbWorkcaseDeviceDTO filter) {
List<TbWorkcaseDeviceDTO> list = workcaseDeviceMapper.selectWorkcaseDeviceList(filter);
return ResultDomain.success("查询成功", list);
}
@Override
public ResultDomain<TbWorkcaseDeviceDTO> getWorkcaseDevicePage(PageRequest<TbWorkcaseDeviceDTO> pageRequest) {
TbWorkcaseDeviceDTO filter = pageRequest.getFilter();
PageParam pageParam = pageRequest.getPageParam();
List<TbWorkcaseDeviceDTO> list = workcaseDeviceMapper.selectWorkcaseDevicePage(filter, pageParam);
long total = workcaseDeviceMapper.countWorkcaseDevices(filter);
pageParam.setTotal((int) total);
PageDomain<TbWorkcaseDeviceDTO> pageDomain = new PageDomain<>(pageParam, list);
return ResultDomain.success("查询成功", pageDomain);
}
}

View File

@@ -5,18 +5,22 @@ server:
# context-path: /urban-lifeline/workcase # 微服务架构下context-path由Gateway管理
# ================== Auth ====================
urban-lifeline:
auth:
enabled: true
whitelist:
- /swagger-ui/**
- /swagger-ui.html
- /v3/api-docs/**
- /webjars/**
- /favicon.ico
- /error
- /actuator/health
- /actuator/info
auth:
enabled: true
gate-way: true
whitelist:
- /swagger-ui/**
- /swagger-ui.html
- /v3/api-docs/**
- /webjars/**
- /favicon.ico
- /error
- /actuator/health
- /actuator/info
# 微信客服回调接口(无需鉴权)
- /workcase/chat/kefu/callback
# CRM回调接口无需鉴权但需签名验证
- /workcase/receive/crm
security:
aes:

View File

@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.workcase.mapper.TbWordCloudMapper">
<resultMap id="BaseResultMap" type="org.xyzh.api.workcase.dto.TbWordCloudDTO">
<id column="word_id" property="wordId" jdbcType="VARCHAR"/>
<result column="word" property="word" jdbcType="VARCHAR"/>
<result column="frequency" property="frequency" jdbcType="VARCHAR"/>
<result column="category" property="category" jdbcType="VARCHAR"/>
<result column="stat_date" property="statDate" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
word_id, word, frequency, category, stat_date, create_time, update_time
</sql>
<insert id="insertWordCloud" parameterType="org.xyzh.api.workcase.dto.TbWordCloudDTO">
INSERT INTO workcase.tb_word_cloud (
word_id, word, stat_date
<if test="frequency != null">, frequency</if>
<if test="category != null">, category</if>
) VALUES (
#{wordId}, #{word}, #{statDate}::date
<if test="frequency != null">, #{frequency}</if>
<if test="category != null">, #{category}</if>
)
</insert>
<update id="updateWordCloud" parameterType="org.xyzh.api.workcase.dto.TbWordCloudDTO">
UPDATE workcase.tb_word_cloud
<set>
<if test="word != null and word != ''">word = #{word},</if>
<if test="frequency != null">frequency = #{frequency},</if>
<if test="category != null">category = #{category},</if>
update_time = now()
</set>
WHERE word_id = #{wordId}
</update>
<select id="selectWordCloudById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM workcase.tb_word_cloud
WHERE word_id = #{wordId}
</select>
<select id="selectWordCloudOne" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM workcase.tb_word_cloud
<where>
<if test="filter.wordId != null and filter.wordId != ''">
AND word_id = #{filter.wordId}
</if>
<if test="filter.word != null and filter.word != ''">
AND word = #{filter.word}
</if>
<if test="filter.category != null and filter.category != ''">
AND category = #{filter.category}
</if>
<if test="filter.statDate != null and filter.statDate != ''">
AND stat_date = #{filter.statDate}::date
</if>
</where>
LIMIT 1
</select>
<select id="selectWordCloudList" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM workcase.tb_word_cloud
<where>
<if test="filter.wordId != null and filter.wordId != ''">
AND word_id = #{filter.wordId}
</if>
<if test="filter.word != null and filter.word != ''">
AND word LIKE CONCAT('%', #{filter.word}, '%')
</if>
<if test="filter.category != null and filter.category != ''">
AND category = #{filter.category}
</if>
<if test="filter.statDate != null and filter.statDate != ''">
AND stat_date = #{filter.statDate}::date
</if>
</where>
ORDER BY frequency DESC, create_time DESC
</select>
<select id="selectWordCloudPage" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM workcase.tb_word_cloud
<where>
<if test="filter.wordId != null and filter.wordId != ''">
AND word_id = #{filter.wordId}
</if>
<if test="filter.word != null and filter.word != ''">
AND word LIKE CONCAT('%', #{filter.word}, '%')
</if>
<if test="filter.category != null and filter.category != ''">
AND category = #{filter.category}
</if>
<if test="filter.statDate != null and filter.statDate != ''">
AND stat_date = #{filter.statDate}::date
</if>
</where>
ORDER BY frequency DESC, create_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select>
<select id="countWordClouds" resultType="long">
SELECT COUNT(*)
FROM workcase.tb_word_cloud
<where>
<if test="filter.wordId != null and filter.wordId != ''">
AND word_id = #{filter.wordId}
</if>
<if test="filter.word != null and filter.word != ''">
AND word LIKE CONCAT('%', #{filter.word}, '%')
</if>
<if test="filter.category != null and filter.category != ''">
AND category = #{filter.category}
</if>
<if test="filter.statDate != null and filter.statDate != ''">
AND stat_date = #{filter.statDate}::date
</if>
</where>
</select>
<update id="incrementFrequency">
UPDATE workcase.tb_word_cloud
SET frequency = frequency + #{count}, update_time = now()
WHERE word_id = #{wordId}
</update>
</mapper>

View File

@@ -4,9 +4,10 @@
2. 当连续3次ai聊天后询问是否转人工
3. 用户触发转人工(可能是一开始,就手动触发,没有聊天记录),跳转到微信客服的功能服务
4. 用户跳转前,必须创建工单
5. 同步工单到CRM
6. 将工单信息作为微信客服的欢迎语,进行放送,让来客和员工能看到工单的基本信息,从而实现让员工知道工单是哪一个
5. ai根据聊天对话自动生成部分工单信息预填入小程序的工单创建的表单
6. 创建工单后同步工单到CRM
7. 将工单信息作为微信客服的欢迎语,进行放送,让来客和员工能看到工单的基本信息,从而实现让员工知道工单是哪一个
<!-- 员工可前往网页查看工单和相关的聊天记录ai和来客的对话以及员工和来客的对话 -->
7. 同步用户和员工在微信客服上的聊天记录同步到tb_chat表里面对话人员是来客和客服。把ai替换成员工进行对话的续接
8. 员工自己更新工单状态如果在CRM更新工单状态会触发receiveWorkcaseFromCrm如果在本系统更新工单会触发工单同步到CRM
9. 在工单是完成、撤销后,工单、对话进行总结,并更新词云
8. 同步用户和员工在微信客服上的聊天记录同步到tb_chat表里面对话人员是来客和客服。把ai替换成员工进行对话的续接
9. 员工自己更新工单状态如果在CRM更新工单状态会触发receiveWorkcaseFromCrm如果在本系统更新工单会触发工单同步到CRM
10. 在工单是完成、撤销后,工单、对话进行总结,并更新词云