客服模块
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
|
||||
@@ -42,13 +42,6 @@ public interface WorkcaseChatService {
|
||||
*/
|
||||
ResultDomain<TbChat> getChatList(TbChat filter);
|
||||
|
||||
/**
|
||||
* 获取聊天分页
|
||||
* @param pageRequest
|
||||
* @return 聊天分页
|
||||
*/
|
||||
ResultDomain<TbChat> getChatPage(PageRequest<TbChat> pageRequest);
|
||||
|
||||
// ========================= 聊天信息管理 ======================
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
// ====================== 工单处理过程 ======================
|
||||
/**
|
||||
|
||||
56
urbanLifelineServ/common/common-wechat/pom.xml
Normal file
56
urbanLifelineServ/common/common-wechat/pom.xml
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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) {}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
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);
|
||||
}
|
||||
|
||||
@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;
|
||||
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分析对话,自动生成工单预填信息 ==============
|
||||
// 步骤5:AI根据聊天对话,自动生成部分工单信息,预填入小程序的工单创建表单
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
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");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbWorkcaseDeviceDTO> createWorkcaseDevice(TbWorkcaseDeviceDTO workcaseDevice) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
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);
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbWorkcaseProcessDTO> createWorkcaseProcess(TbWorkcaseProcessDTO workcaseProcess) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
syncWorkcaseToCrm(workcase);
|
||||
sendWechatKefuWelcome(workcase);
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbWorkcaseDTO> deleteWorkcase(TbWorkcaseDTO workcase) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
return ResultDomain.success("创建成功", workcase);
|
||||
}
|
||||
|
||||
@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;
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ server:
|
||||
# context-path: /urban-lifeline/workcase # 微服务架构下,context-path由Gateway管理
|
||||
|
||||
# ================== Auth ====================
|
||||
urban-lifeline:
|
||||
auth:
|
||||
auth:
|
||||
enabled: true
|
||||
gate-way: true
|
||||
whitelist:
|
||||
- /swagger-ui/**
|
||||
- /swagger-ui.html
|
||||
@@ -17,6 +17,10 @@ urban-lifeline:
|
||||
- /error
|
||||
- /actuator/health
|
||||
- /actuator/info
|
||||
# 微信客服回调接口(无需鉴权)
|
||||
- /workcase/chat/kefu/callback
|
||||
# CRM回调接口(无需鉴权,但需签名验证)
|
||||
- /workcase/receive/crm
|
||||
|
||||
security:
|
||||
aes:
|
||||
|
||||
@@ -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>
|
||||
@@ -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. 在工单是完成、撤销后,工单、对话进行总结,并更新词云
|
||||
Reference in New Issue
Block a user