From 5f2301e16c0ca8141973acb34c088e4c61f429b9 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Fri, 19 Dec 2025 14:37:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=A2=E6=9C=8D=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../database/postgres/sql/initDataConfig.sql | 10 +- .../workcase/service/WorkcaseChatService.java | 7 - .../api/workcase/service/WorkcaseService.java | 4 +- .../common/common-wechat/pom.xml | 56 ++ .../wechat/config/WeChatKefuConfig.java | 31 ++ .../common/wechat/config/WeChatKefuInit.java | 69 +++ .../handler/DefaultKefuCallbackHandler.java | 96 ++++ .../wechat/handler/KefuCallbackHandler.java | 42 ++ .../kefu/account/KefuAccountHandler.java | 28 + .../kefu/account/KefuAccountService.java | 183 +++++++ .../kefu/core/KefuAccessTokenManager.java | 109 ++++ .../wechat/kefu/info/KefuInfoHandler.java | 20 + .../wechat/kefu/info/KefuInfoService.java | 100 ++++ .../kefu/message/KefuMessageHandler.java | 51 ++ .../kefu/message/KefuMessageService.java | 223 ++++++++ .../wechat/pojo/kefu/AccessTokenResponse.java | 21 + .../common/wechat/pojo/kefu/KefuAccount.java | 26 + .../pojo/kefu/KefuAccountAddResponse.java | 19 + .../pojo/kefu/KefuAccountListResponse.java | 20 + .../common/wechat/pojo/kefu/KefuCallback.java | 47 ++ .../common/wechat/pojo/kefu/KefuCustomer.java | 25 + .../pojo/kefu/KefuCustomerResponse.java | 22 + .../common/wechat/pojo/kefu/KefuEvent.java | 29 + .../wechat/pojo/kefu/KefuLinkResponse.java | 19 + .../common/wechat/pojo/kefu/KefuMessage.java | 57 ++ .../wechat/pojo/kefu/KefuSendMsgResponse.java | 19 + .../wechat/pojo/kefu/KefuSyncMsgResponse.java | 139 +++++ .../wechat/pojo/kefu/MediaUploadResponse.java | 23 + .../wechat/pojo/kefu/WeChatResponse.java | 23 + urbanLifelineServ/common/pom.xml | 6 + urbanLifelineServ/pom.xml | 5 + urbanLifelineServ/workcase/pom.xml | 4 + .../workcase/config/WeChatKefuConfig.java | 32 ++ .../controller/WorkcaseChatContorller.java | 229 ++++++++ .../controller/WorkcaseController.java | 233 +++++++++ .../workcase/handler/WorkcaseKefuHandler.java | 120 +++++ .../workcase/mapper/TbWordCloudMapper.java | 60 +++ .../service/WorkcaseChatServiceImpl.java | 335 +++++++++--- .../workcase/service/WorkcaseServiceImpl.java | 495 ++++++++++++++---- .../src/main/resources/application.yml | 28 +- .../resources/mapper/TbWordCloudMapper.xml | 134 +++++ urbanLifelineServ/workcase/工单流程.md | 11 +- 42 files changed, 3012 insertions(+), 198 deletions(-) create mode 100644 urbanLifelineServ/common/common-wechat/pom.xml create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatKefuConfig.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatKefuInit.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/handler/DefaultKefuCallbackHandler.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/handler/KefuCallbackHandler.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/account/KefuAccountHandler.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/account/KefuAccountService.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/core/KefuAccessTokenManager.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/info/KefuInfoHandler.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/info/KefuInfoService.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/message/KefuMessageHandler.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/message/KefuMessageService.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/AccessTokenResponse.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccount.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccountAddResponse.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccountListResponse.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCallback.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCustomer.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCustomerResponse.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuEvent.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuLinkResponse.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuMessage.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuSendMsgResponse.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuSyncMsgResponse.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/MediaUploadResponse.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/WeChatResponse.java create mode 100644 urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WeChatKefuConfig.java create mode 100644 urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java create mode 100644 urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java create mode 100644 urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/handler/WorkcaseKefuHandler.java create mode 100644 urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbWordCloudMapper.java create mode 100644 urbanLifelineServ/workcase/src/main/resources/mapper/TbWordCloudMapper.xml diff --git a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql index a50b8327..bab83223 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql @@ -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); diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseChatService.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseChatService.java index 84afce79..1ba64c5f 100644 --- a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseChatService.java +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseChatService.java @@ -42,13 +42,6 @@ public interface WorkcaseChatService { */ ResultDomain getChatList(TbChat filter); - /** - * 获取聊天分页 - * @param pageRequest - * @return 聊天分页 - */ - ResultDomain getChatPage(PageRequest pageRequest); - // ========================= 聊天信息管理 ====================== /** diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseService.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseService.java index 868727c2..9b3e75c7 100644 --- a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseService.java +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseService.java @@ -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 receiveWorkcaseFromCrm(JSON json); + ResultDomain receiveWorkcaseFromCrm(JSONObject json); // ====================== 工单处理过程 ====================== /** diff --git a/urbanLifelineServ/common/common-wechat/pom.xml b/urbanLifelineServ/common/common-wechat/pom.xml new file mode 100644 index 00000000..d389d4d8 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + org.xyzh + common + 1.0.0 + + + org.xyzh.common + common-wechat + ${urban-lifeline.version} + jar + + + 21 + 21 + + + + + org.xyzh.apis + api-system + + + + org.xyzh.common + common-core + + + + org.apache.dubbo + dubbo-spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-web + + + + com.alibaba.fastjson2 + fastjson2 + + + + org.projectlombok + lombok + provided + + + + + \ No newline at end of file diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatKefuConfig.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatKefuConfig.java new file mode 100644 index 00000000..aebc21fb --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatKefuConfig.java @@ -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; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatKefuInit.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatKefuInit.java new file mode 100644 index 00000000..fa1208e3 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatKefuInit.java @@ -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 result = sysConfigService.getConfigValueByKey(key); + if (result != null && result.getSuccess()) { + return result.getData(); + } + return null; + } + + public static WeChatKefuConfig getConfig() { + return weChatConfig; + } + + public void refreshConfig() { + loadConfig(); + } +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/handler/DefaultKefuCallbackHandler.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/handler/DefaultKefuCallbackHandler.java new file mode 100644 index 00000000..ab4344cb --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/handler/DefaultKefuCallbackHandler.java @@ -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()); + } + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/handler/KefuCallbackHandler.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/handler/KefuCallbackHandler.java new file mode 100644 index 00000000..4b6d9c97 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/handler/KefuCallbackHandler.java @@ -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); + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/account/KefuAccountHandler.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/account/KefuAccountHandler.java new file mode 100644 index 00000000..53f06bcc --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/account/KefuAccountHandler.java @@ -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) {} + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/account/KefuAccountService.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/account/KefuAccountService.java new file mode 100644 index 00000000..bd0b3248 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/account/KefuAccountService.java @@ -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 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 entity = new HttpEntity<>(json, headers); + return restTemplate.postForObject(url, entity, String.class); + } + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/core/KefuAccessTokenManager.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/core/KefuAccessTokenManager.java new file mode 100644 index 00000000..19fb4d7c --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/core/KefuAccessTokenManager.java @@ -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 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; + } + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/info/KefuInfoHandler.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/info/KefuInfoHandler.java new file mode 100644 index 00000000..173872ec --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/info/KefuInfoHandler.java @@ -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) {} + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/info/KefuInfoService.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/info/KefuInfoService.java new file mode 100644 index 00000000..b2615e59 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/info/KefuInfoService.java @@ -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 getCustomerInfo(List 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 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 entity = new HttpEntity<>(json, headers); + return restTemplate.postForObject(url, entity, String.class); + } + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/message/KefuMessageHandler.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/message/KefuMessageHandler.java new file mode 100644 index 00000000..da5bc693 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/message/KefuMessageHandler.java @@ -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) {} + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/message/KefuMessageService.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/message/KefuMessageService.java new file mode 100644 index 00000000..a81682fc --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/kefu/message/KefuMessageService.java @@ -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 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 entity = new HttpEntity<>(json, headers); + return restTemplate.postForObject(url, entity, String.class); + } + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/AccessTokenResponse.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/AccessTokenResponse.java new file mode 100644 index 00000000..221e41e3 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/AccessTokenResponse.java @@ -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; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccount.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccount.java new file mode 100644 index 00000000..572207cb --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccount.java @@ -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 receptionistIdList; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccountAddResponse.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccountAddResponse.java new file mode 100644 index 00000000..092c7e79 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccountAddResponse.java @@ -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; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccountListResponse.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccountListResponse.java new file mode 100644 index 00000000..af6abf19 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuAccountListResponse.java @@ -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 accountList; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCallback.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCallback.java new file mode 100644 index 00000000..b21dde44 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCallback.java @@ -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; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCustomer.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCustomer.java new file mode 100644 index 00000000..da7a74d5 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCustomer.java @@ -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; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCustomerResponse.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCustomerResponse.java new file mode 100644 index 00000000..de8aa70c --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuCustomerResponse.java @@ -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 customerList; + + private String invalidExternalUserid; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuEvent.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuEvent.java new file mode 100644 index 00000000..1eb2b5b4 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuEvent.java @@ -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; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuLinkResponse.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuLinkResponse.java new file mode 100644 index 00000000..641bb189 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuLinkResponse.java @@ -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; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuMessage.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuMessage.java new file mode 100644 index 00000000..eb2b8e1f --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuMessage.java @@ -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; + } + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuSendMsgResponse.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuSendMsgResponse.java new file mode 100644 index 00000000..3f19a498 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuSendMsgResponse.java @@ -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; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuSyncMsgResponse.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuSyncMsgResponse.java new file mode 100644 index 00000000..cf06e699 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/KefuSyncMsgResponse.java @@ -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 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; + } + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/MediaUploadResponse.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/MediaUploadResponse.java new file mode 100644 index 00000000..43d49cca --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/MediaUploadResponse.java @@ -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; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/WeChatResponse.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/WeChatResponse.java new file mode 100644 index 00000000..fd66e222 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/kefu/WeChatResponse.java @@ -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; + } + +} diff --git a/urbanLifelineServ/common/pom.xml b/urbanLifelineServ/common/pom.xml index 8bd0be38..e3e5d983 100644 --- a/urbanLifelineServ/common/pom.xml +++ b/urbanLifelineServ/common/pom.xml @@ -22,6 +22,7 @@ common-jdbc common-all common-exception + common-wechat @@ -71,6 +72,11 @@ common-jdbc ${urban-lifeline.version} + + org.xyzh.common + common-wechat + ${urban-lifeline.version} + \ No newline at end of file diff --git a/urbanLifelineServ/pom.xml b/urbanLifelineServ/pom.xml index cd9ed4da..10032083 100644 --- a/urbanLifelineServ/pom.xml +++ b/urbanLifelineServ/pom.xml @@ -300,6 +300,11 @@ common-jdbc ${urban-lifeline.version} + + org.xyzh.common + common-wechat + ${urban-lifeline.version} + org.xyzh diff --git a/urbanLifelineServ/workcase/pom.xml b/urbanLifelineServ/workcase/pom.xml index fbe77054..dc66cf6c 100644 --- a/urbanLifelineServ/workcase/pom.xml +++ b/urbanLifelineServ/workcase/pom.xml @@ -42,6 +42,10 @@ org.xyzh.common common-exception + + org.xyzh.common + common-wechat + org.springframework.boot spring-boot-starter-web diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WeChatKefuConfig.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WeChatKefuConfig.java new file mode 100644 index 00000000..b637fcca --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/WeChatKefuConfig.java @@ -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); + } + +} diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java new file mode 100644 index 00000000..8e9b21ac --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatContorller.java @@ -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 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 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 getChatList(@RequestBody TbChat filter) { + return workcaseChatService.getChatList(filter); + } + + @Operation(summary = "获取对话消息列表") + @PostMapping("/message/list") + public ResultDomain 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 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 stopChat(@RequestBody TbChat filter, @PathVariable String taskId) { + return workcaseChatService.stopChatMessageByTaskId(filter, taskId); + } + + @Operation(summary = "评论对话消息") + @PostMapping("/comment") + public ResultDomain commentChatMessage(@RequestBody TbChat filter, + @RequestParam String messageId, @RequestParam String comment) { + return workcaseChatService.commentChatMessage(filter, messageId, comment); + } + + // ========================= 对话分析 ========================= + + @Operation(summary = "分析对话(AI预填工单信息)") + @GetMapping("/analyze/{chatId}") + public ResultDomain analyzeChat(@PathVariable String chatId) { + return workcaseChatService.analyzeChat(chatId); + } + + @Operation(summary = "总结对话") + @PostMapping("/summary/{chatId}") + public ResultDomain 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 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 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 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 getWordCloudList(@RequestBody TbWordCloudDTO filter) { + return workcaseChatService.getWordCloudList(filter); + } + + @Operation(summary = "分页查询词云") + @PostMapping("/wordcloud/page") + public ResultDomain getWordCloudPage(@RequestBody PageRequest 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); + } + +} diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java new file mode 100644 index 00000000..91046f84 --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java @@ -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 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 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 deleteWorkcase(@PathVariable String workcaseId) { + TbWorkcaseDTO workcase = new TbWorkcaseDTO(); + workcase.setWorkcaseId(workcaseId); + return workcaseService.deleteWorkcase(workcase); + } + + @Operation(summary = "获取工单详情") + @GetMapping("/{workcaseId}") + public ResultDomain getWorkcaseById(@PathVariable String workcaseId) { + return workcaseService.getWorkcaseById(workcaseId); + } + + @Operation(summary = "查询工单列表") + @PostMapping("/list") + public ResultDomain getWorkcaseList(@RequestBody TbWorkcaseDTO filter) { + return workcaseService.getWorkcaseList(filter); + } + + @Operation(summary = "分页查询工单") + @PostMapping("/page") + public ResultDomain getWorkcasePage(@RequestBody PageRequest 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 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 receiveWorkcaseFromCrm(@RequestBody String jsonBody) { + JSONObject json = JSONObject.parseObject(jsonBody); + return workcaseService.receiveWorkcaseFromCrm(json); + } + + // ========================= 工单处理过程 ========================= + + @Operation(summary = "创建工单处理过程") + @PostMapping("/process") + public ResultDomain 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 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 deleteWorkcaseProcess(@PathVariable String processId) { + TbWorkcaseProcessDTO process = new TbWorkcaseProcessDTO(); + process.setProcessId(processId); + return workcaseService.deleteWorkcaseProcess(process); + } + + @Operation(summary = "查询工单处理过程列表") + @PostMapping("/process/list") + public ResultDomain getWorkcaseProcessList(@RequestBody TbWorkcaseProcessDTO filter) { + return workcaseService.getWorkcaseProcessList(filter); + } + + @Operation(summary = "分页查询工单处理过程") + @PostMapping("/process/page") + public ResultDomain getWorkcaseProcessPage(@RequestBody PageRequest 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 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 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 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 getWorkcaseDeviceList(@RequestBody TbWorkcaseDeviceDTO filter) { + return workcaseService.getWorkcaseDeviceList(filter); + } + + @Operation(summary = "分页查询工单设备") + @PostMapping("/device/page") + public ResultDomain getWorkcaseDevicePage(@RequestBody PageRequest 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); + } + +} diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/handler/WorkcaseKefuHandler.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/handler/WorkcaseKefuHandler.java new file mode 100644 index 00000000..42720d35 --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/handler/WorkcaseKefuHandler.java @@ -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(); + } + +} diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbWordCloudMapper.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbWordCloudMapper.java new file mode 100644 index 00000000..544879d6 --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbWordCloudMapper.java @@ -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 selectWordCloudList(@Param("filter") TbWordCloudDTO filter); + + /** + * 分页查询词云 + */ + List 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); + +} diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseChatServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseChatServiceImpl.java index 1b7f34fd..1d17146e 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseChatServiceImpl.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseChatServiceImpl.java @@ -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 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 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 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 createChat(TbChat chat) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getChatList(TbChat filter) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getChatMessageList(TbChat filter) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getChatPage(PageRequest pageRequest) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getWordCloudList(TbWordCloudDTO filter) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getWordCloudPage(PageRequest pageRequest) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain prepareChatMessageSession(ChatPrepareData prepareData) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain 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 summaryChat(String chatId) { - // TODO Auto-generated method stub - return null; + logger.info("创建对话: userId={}", chat.getUserId()); + ResultDomain result = agentChatService.createChat(chat); + if (result.getSuccess() && result.getData() != null) { + redisService.set(CHAT_COUNT_KEY_PREFIX + result.getData().getChatId(), 0); + } + return result; } @Override public ResultDomain updateChat(TbChat chat) { - // TODO Auto-generated method stub - return null; + logger.info("更新对话: chatId={}", chat.getChatId()); + return agentChatService.updateChat(chat); + } + + @Override + public ResultDomain getChatList(TbChat filter) { + return agentChatService.getChatList(filter); + } + + // ========================= 聊天信息管理 ====================== + + @Override + public ResultDomain getChatMessageList(TbChat filter) { + return agentChatService.getChatMessageList(filter); + } + + @Override + public ResultDomain 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 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 stopChatMessageByTaskId(TbChat filter, String taskId) { + return agentChatService.stopChatMessageByTaskId(filter, taskId); + } + + @Override + public ResultDomain commentChatMessage(TbChat filter, String messageId, String comment) { + return agentChatService.commentChatMessage(filter, messageId, comment); + } + + // =============================== 对话分析 ========================== + + @Override + public ResultDomain analyzeChat(String chatId) { + logger.info("分析对话内容,生成工单预填信息: chatId={}", chatId); + + TbChat filter = new TbChat(); + filter.setChatId(chatId); + ResultDomain msgResult = agentChatService.getChatMessageList(filter); + + if (!msgResult.getSuccess() || msgResult.getDataList() == null || msgResult.getDataList().isEmpty()) { + return ResultDomain.failure("获取对话消息失败或消息为空"); + } + + List 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 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 summaryChat(String chatId) { + logger.info("总结对话: chatId={}", chatId); + + TbChat filter = new TbChat(); + filter.setChatId(chatId); + ResultDomain msgResult = agentChatService.getChatMessageList(filter); + + if (!msgResult.getSuccess() || msgResult.getDataList() == null) { + return ResultDomain.failure("获取对话消息失败"); + } + + List messages = msgResult.getDataList(); + + // TODO: 调用AI进行对话总结,提取关键词更新词云 + // 伪代码: + // String summary = aiService.summarize(messages); + // List keywords = aiService.extractKeywords(messages); + // updateWordCloud(keywords); + + extractAndUpdateWordCloud(messages); + + TbWorkcaseDTO summary = new TbWorkcaseDTO(); + logger.info("对话总结完成: chatId={}", chatId); + return ResultDomain.success("对话总结完成", summary); + } + + private void extractAndUpdateWordCloud(List messages) { + // TODO: 调用AI提取关键词 + // 伪代码:List 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 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 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 getWordCloudList(TbWordCloudDTO filter) { + List list = wordCloudMapper.selectWordCloudList(filter); + return ResultDomain.success("查询成功", list); + } + + @Override + public ResultDomain getWordCloudPage(PageRequest pageRequest) { + TbWordCloudDTO filter = pageRequest.getFilter(); + PageParam pageParam = pageRequest.getPageParam(); + + List list = wordCloudMapper.selectWordCloudPage(filter, pageParam); + long total = wordCloudMapper.countWordClouds(filter); + + pageParam.setTotal((int) total); + PageDomain pageDomain = new PageDomain<>(pageParam, list); + return ResultDomain.success("查询成功", pageDomain); } - - } diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java index 889c43b1..f7e18e02 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java @@ -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 createWorkcase(TbWorkcaseDTO workcase) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain createWorkcaseDevice(TbWorkcaseDeviceDTO workcaseDevice) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain createWorkcaseProcess(TbWorkcaseProcessDTO workcaseProcess) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain deleteWorkcase(TbWorkcaseDTO workcase) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain deleteWorkcaseDevice(TbWorkcaseDeviceDTO workcaseDevice) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain deleteWorkcaseProcess(TbWorkcaseProcessDTO workcaseProcess) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getWorkcaseById(String workcaseId) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getWorkcaseDeviceList(TbWorkcaseDeviceDTO filter) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getWorkcaseDevicePage(PageRequest pageRequest) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getWorkcaseList(TbWorkcaseDTO filter) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getWorkcasePage(PageRequest pageRequest) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getWorkcaseProcessList(TbWorkcaseProcessDTO filter) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain getWorkcaseProcessPage(PageRequest pageRequest) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain receiveWorkcaseFromCrm(JSON json) { - // TODO Auto-generated method stub - return null; - } - - @Override - public ResultDomain syncWorkcaseToCrm(TbWorkcaseDTO workcase) { - // TODO Auto-generated method stub - return null; + logger.info("创建工单: userId={}, type={}", workcase.getUserId(), workcase.getType()); + + if (workcase.getWorkcaseId() == null || workcase.getWorkcaseId().isEmpty()) { + workcase.setWorkcaseId(IdUtil.generateUUID()); + } + if (workcase.getOptsn() == null || workcase.getOptsn().isEmpty()) { + workcase.setOptsn(IdUtil.getOptsn()); + } + if (workcase.getStatus() == null || workcase.getStatus().isEmpty()) { + workcase.setStatus("pending"); + } + if (workcase.getEmergency() == null || workcase.getEmergency().isEmpty()) { + workcase.setEmergency("normal"); + } + + int rows = workcaseMapper.insertWorkcase(workcase); + if (rows > 0) { + TbWorkcaseProcessDTO process = new TbWorkcaseProcessDTO(); + process.setProcessId(IdUtil.generateUUID()); + process.setOptsn(IdUtil.getOptsn()); + process.setWorkcaseId(workcase.getWorkcaseId()); + process.setAction(WorkcaseProcessAction.INFO.getName()); + process.setMessage("工单创建"); + process.setCreator(workcase.getCreator()); + workcaseProcessMapper.insertWorkcaseProcess(process); + + syncWorkcaseToCrm(workcase); + sendWechatKefuWelcome(workcase); + + return ResultDomain.success("创建成功", workcase); + } + return ResultDomain.failure("创建失败"); } @Override public ResultDomain 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 updateWorkcaseDevice(TbWorkcaseDeviceDTO workcaseDevice) { - // TODO Auto-generated method stub - return null; + public ResultDomain 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 getWorkcaseById(String workcaseId) { + TbWorkcaseDTO workcase = workcaseMapper.selectWorkcaseById(workcaseId); + if (workcase != null) { + return ResultDomain.success("查询成功", workcase); + } + return ResultDomain.failure("工单不存在"); + } + + @Override + public ResultDomain getWorkcaseList(TbWorkcaseDTO filter) { + List list = workcaseMapper.selectWorkcaseList(filter); + return ResultDomain.success("查询成功", list); + } + + @Override + public ResultDomain getWorkcasePage(PageRequest pageRequest) { + TbWorkcaseDTO filter = pageRequest.getFilter(); + PageParam pageParam = pageRequest.getPageParam(); + + List list = workcaseMapper.selectWorkcasePage(filter, pageParam); + long total = workcaseMapper.countWorkcases(filter); + + pageParam.setTotal((int) total); + PageDomain pageDomain = new PageDomain<>(pageParam, list); + return ResultDomain.success("查询成功", pageDomain); + } + + // ====================== 同步到CRM和接收 =================== + + @Override + public ResultDomain 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 request = new HttpEntity<>(crmData.toJSONString(), headers); + // ResponseEntity 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 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 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 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 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 getWorkcaseProcessList(TbWorkcaseProcessDTO filter) { + List list = workcaseProcessMapper.selectWorkcaseProcessList(filter); + return ResultDomain.success("查询成功", list); + } + + @Override + public ResultDomain getWorkcaseProcessPage(PageRequest pageRequest) { + TbWorkcaseProcessDTO filter = pageRequest.getFilter(); + PageParam pageParam = pageRequest.getPageParam(); + + List list = workcaseProcessMapper.selectWorkcaseProcessPage(filter, pageParam); + long total = workcaseProcessMapper.countWorkcaseProcesses(filter); + + pageParam.setTotal((int) total); + PageDomain pageDomain = new PageDomain<>(pageParam, list); + return ResultDomain.success("查询成功", pageDomain); + } + + // ====================== 工单设备管理 ====================== + + @Override + public ResultDomain 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 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 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 getWorkcaseDeviceList(TbWorkcaseDeviceDTO filter) { + List list = workcaseDeviceMapper.selectWorkcaseDeviceList(filter); + return ResultDomain.success("查询成功", list); + } + + @Override + public ResultDomain getWorkcaseDevicePage(PageRequest pageRequest) { + TbWorkcaseDeviceDTO filter = pageRequest.getFilter(); + PageParam pageParam = pageRequest.getPageParam(); + + List list = workcaseDeviceMapper.selectWorkcaseDevicePage(filter, pageParam); + long total = workcaseDeviceMapper.countWorkcaseDevices(filter); + + pageParam.setTotal((int) total); + PageDomain pageDomain = new PageDomain<>(pageParam, list); + return ResultDomain.success("查询成功", pageDomain); } - } diff --git a/urbanLifelineServ/workcase/src/main/resources/application.yml b/urbanLifelineServ/workcase/src/main/resources/application.yml index bd9f0260..8638f91a 100644 --- a/urbanLifelineServ/workcase/src/main/resources/application.yml +++ b/urbanLifelineServ/workcase/src/main/resources/application.yml @@ -5,18 +5,22 @@ server: # context-path: /urban-lifeline/workcase # 微服务架构下,context-path由Gateway管理 # ================== Auth ==================== -urban-lifeline: - auth: - enabled: true - whitelist: - - /swagger-ui/** - - /swagger-ui.html - - /v3/api-docs/** - - /webjars/** - - /favicon.ico - - /error - - /actuator/health - - /actuator/info +auth: + enabled: true + gate-way: true + whitelist: + - /swagger-ui/** + - /swagger-ui.html + - /v3/api-docs/** + - /webjars/** + - /favicon.ico + - /error + - /actuator/health + - /actuator/info + # 微信客服回调接口(无需鉴权) + - /workcase/chat/kefu/callback + # CRM回调接口(无需鉴权,但需签名验证) + - /workcase/receive/crm security: aes: diff --git a/urbanLifelineServ/workcase/src/main/resources/mapper/TbWordCloudMapper.xml b/urbanLifelineServ/workcase/src/main/resources/mapper/TbWordCloudMapper.xml new file mode 100644 index 00000000..a635f9f0 --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/resources/mapper/TbWordCloudMapper.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + word_id, word, frequency, category, stat_date, create_time, update_time + + + + INSERT INTO workcase.tb_word_cloud ( + word_id, word, stat_date + , frequency + , category + ) VALUES ( + #{wordId}, #{word}, #{statDate}::date + , #{frequency} + , #{category} + ) + + + + UPDATE workcase.tb_word_cloud + + word = #{word}, + frequency = #{frequency}, + category = #{category}, + update_time = now() + + WHERE word_id = #{wordId} + + + + + + + + + + + + + + UPDATE workcase.tb_word_cloud + SET frequency = frequency + #{count}, update_time = now() + WHERE word_id = #{wordId} + + + diff --git a/urbanLifelineServ/workcase/工单流程.md b/urbanLifelineServ/workcase/工单流程.md index 83f4382b..943dc531 100644 --- a/urbanLifelineServ/workcase/工单流程.md +++ b/urbanLifelineServ/workcase/工单流程.md @@ -4,9 +4,10 @@ 2. 当连续3次ai聊天后,询问是否转人工 3. 用户触发转人工(可能是一开始,就手动触发,没有聊天记录),跳转到微信客服的功能服务 4. 用户跳转前,必须创建工单 - 5. 同步工单到CRM - 6. 将工单信息作为微信客服的欢迎语,进行放送,让来客和员工能看到工单的基本信息,从而实现让员工知道工单是哪一个 + 5. ai根据聊天对话,自动生成部分工单信息,预填入小程序的工单创建的表单, + 6. 创建工单后,同步工单到CRM + 7. 将工单信息作为微信客服的欢迎语,进行放送,让来客和员工能看到工单的基本信息,从而实现让员工知道工单是哪一个 - 7. 同步用户和员工在微信客服上的聊天记录,同步到tb_chat表里面,对话人员是来客和客服。(把ai替换成员工进行对话的续接) - 8. 员工自己更新工单状态,如果在CRM更新工单状态会触发receiveWorkcaseFromCrm,如果在本系统更新工单会触发工单同步到CRM - 9. 在工单是完成、撤销后,工单、对话进行总结,并更新词云 \ No newline at end of file + 8. 同步用户和员工在微信客服上的聊天记录,同步到tb_chat表里面,对话人员是来客和客服。(把ai替换成员工进行对话的续接) + 9. 员工自己更新工单状态,如果在CRM更新工单状态会触发receiveWorkcaseFromCrm,如果在本系统更新工单会触发工单同步到CRM + 10. 在工单是完成、撤销后,工单、对话进行总结,并更新词云 \ No newline at end of file