Selaa lähdekoodia

1、迁移ai回复模块,调整ai配置,测试ai回复信息

yys 4 viikkoa sitten
vanhempi
commit
7a476f8547
71 muutettua tiedostoa jossa 4818 lisäystä ja 40 poistoa
  1. 22 14
      fs-company-app/src/main/java/com/fs/app/controller/CommonController.java
  2. 14 0
      fs-company-app/src/main/java/com/fs/core/config/ThreadPoolConfig.java
  3. 11 1
      fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java
  4. 11 0
      fs-company/src/main/java/com/fs/company/controller/fastGpt/FastGptRoleController.java
  5. 97 0
      fs-company/src/main/java/com/fs/company/controller/im/ImChatMsgController.java
  6. 131 0
      fs-company/src/main/java/com/fs/company/controller/im/ImChatSessionController.java
  7. 74 0
      fs-service/src/main/java/com/fs/app/chat/domain/AppChatHistory.java
  8. 60 0
      fs-service/src/main/java/com/fs/app/chat/domain/AppChatMsg.java
  9. 99 0
      fs-service/src/main/java/com/fs/app/chat/domain/AppUserChatLogs.java
  10. 17 0
      fs-service/src/main/java/com/fs/app/chat/dto/AppUserChatLogsDTO.java
  11. 64 0
      fs-service/src/main/java/com/fs/app/chat/mapper/AppChatHistoryMapper.java
  12. 62 0
      fs-service/src/main/java/com/fs/app/chat/mapper/AppChatMsgMapper.java
  13. 64 0
      fs-service/src/main/java/com/fs/app/chat/mapper/AppUserChatLogsMapper.java
  14. 65 0
      fs-service/src/main/java/com/fs/app/chat/service/IAppChatHistoryService.java
  15. 62 0
      fs-service/src/main/java/com/fs/app/chat/service/IAppChatMsgService.java
  16. 272 0
      fs-service/src/main/java/com/fs/app/chat/service/impl/AppChatHistoryServiceImpl.java
  17. 88 0
      fs-service/src/main/java/com/fs/app/chat/service/impl/AppChatMsgServiceImpl.java
  18. 11 0
      fs-service/src/main/java/com/fs/app/chat/vo/AppUserChatLogsVO.java
  19. 33 0
      fs-service/src/main/java/com/fs/app/cusrole/domain/AppCustomerRole.java
  20. 72 0
      fs-service/src/main/java/com/fs/app/cusrole/domain/AppCustomerRoleMember.java
  21. 98 0
      fs-service/src/main/java/com/fs/app/cusrole/domain/AppFastGptRole.java
  22. 46 0
      fs-service/src/main/java/com/fs/app/cusrole/domain/AppUserCustomerFriendship.java
  23. 20 0
      fs-service/src/main/java/com/fs/app/cusrole/dto/AppCustomerRoleDTO.java
  24. 77 0
      fs-service/src/main/java/com/fs/app/cusrole/mapper/AppCustomerRoleMemberMapper.java
  25. 13 0
      fs-service/src/main/java/com/fs/app/cusrole/mapper/AppFastGptRoleMapper.java
  26. 12 0
      fs-service/src/main/java/com/fs/app/cusrole/mapper/AppUserCustomerFriendshipMapper.java
  27. 69 0
      fs-service/src/main/java/com/fs/app/cusrole/service/IAppCustomerRoleMemberService.java
  28. 9 0
      fs-service/src/main/java/com/fs/app/cusrole/service/IAppFastGptRoleService.java
  29. 7 0
      fs-service/src/main/java/com/fs/app/cusrole/service/IAppUserCustomerFriendshipService.java
  30. 93 0
      fs-service/src/main/java/com/fs/app/cusrole/service/impl/AppCustomerRoleMemberServiceImpl.java
  31. 17 0
      fs-service/src/main/java/com/fs/app/cusrole/service/impl/AppFastGptRoleImpl.java
  32. 11 0
      fs-service/src/main/java/com/fs/app/cusrole/service/impl/AppUserCustomerFriendshipServiceImpl.java
  33. 18 0
      fs-service/src/main/java/com/fs/app/cusrole/vo/AppCustomerRoleVO.java
  34. 8 0
      fs-service/src/main/java/com/fs/company/domain/CompanyUser.java
  35. 2 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java
  36. 2 0
      fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java
  37. 7 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java
  38. 15 0
      fs-service/src/main/java/com/fs/fastGpt/domain/FastGptChatConversation.java
  39. 5 0
      fs-service/src/main/java/com/fs/fastGpt/domain/FastGptRole.java
  40. 5 1
      fs-service/src/main/java/com/fs/fastGpt/mapper/FastGptRoleMapper.java
  41. 26 0
      fs-service/src/main/java/com/fs/fastGpt/param/FastGptRoleNameAndId.java
  42. 6 2
      fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java
  43. 3 0
      fs-service/src/main/java/com/fs/fastGpt/service/IFastGptRoleService.java
  44. 882 2
      fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java
  45. 5 0
      fs-service/src/main/java/com/fs/fastGpt/service/impl/FastGptRoleServiceImpl.java
  46. 226 0
      fs-service/src/main/java/com/fs/fastGpt/utils/TencentAudioUtils.java
  47. 4 0
      fs-service/src/main/java/com/fs/fastGpt/vo/FastGptRoleVO.java
  48. 64 0
      fs-service/src/main/java/com/fs/fastgptApi/util/AiImgUtil.java
  49. 65 0
      fs-service/src/main/java/com/fs/fastgptApi/util/EventLogUtils.java
  50. 11 0
      fs-service/src/main/java/com/fs/his/mapper/FsInquiryOrderMsgMapper.java
  51. 65 0
      fs-service/src/main/java/com/fs/his/service/IAppChatHistoryService.java
  52. 2 0
      fs-service/src/main/java/com/fs/his/service/IFsInquiryOrderMsgService.java
  53. 102 0
      fs-service/src/main/java/com/fs/his/service/OpenImAsyncService.java
  54. 8 1
      fs-service/src/main/java/com/fs/his/service/impl/FsInquiryOrderMsgServiceImpl.java
  55. 89 0
      fs-service/src/main/java/com/fs/im/domain/ImChatMsg.java
  56. 121 0
      fs-service/src/main/java/com/fs/im/domain/ImChatSession.java
  57. 12 0
      fs-service/src/main/java/com/fs/im/dto/OpenImMsgCallBackResponse.java
  58. 65 0
      fs-service/src/main/java/com/fs/im/mapper/ImChatMsgMapper.java
  59. 70 0
      fs-service/src/main/java/com/fs/im/mapper/ImChatSessionMapper.java
  60. 64 0
      fs-service/src/main/java/com/fs/im/service/IImChatMsgService.java
  61. 66 0
      fs-service/src/main/java/com/fs/im/service/IImChatSessionService.java
  62. 2 0
      fs-service/src/main/java/com/fs/im/service/OpenIMService.java
  63. 99 0
      fs-service/src/main/java/com/fs/im/service/impl/ImChatMsgServiceImpl.java
  64. 104 0
      fs-service/src/main/java/com/fs/im/service/impl/ImChatSessionServiceImpl.java
  65. 152 14
      fs-service/src/main/java/com/fs/im/service/impl/OpenIMServiceImpl.java
  66. 34 0
      fs-service/src/main/java/com/fs/im/util/JsonUtil.java
  67. 316 0
      fs-service/src/main/java/com/fs/im/util/OpenIMUtil.java
  68. 4 2
      fs-service/src/main/resources/application-config-zkzh.yml
  69. 7 3
      fs-service/src/main/resources/mapper/fastGpt/FastGptRoleMapper.xml
  70. 136 0
      fs-service/src/main/resources/mapper/im/ImChatMsgMapper.xml
  71. 145 0
      fs-service/src/main/resources/mapper/im/ImChatSessionMapper.xml

+ 22 - 14
fs-company-app/src/main/java/com/fs/app/controller/CommonController.java

@@ -5,7 +5,6 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.jiguang.common.resp.APIConnectionException;
 import cn.jiguang.common.resp.APIRequestException;
 import com.alibaba.fastjson.JSON;
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fs.app.annotation.Login;
 import com.fs.app.utils.CityTreeUtil;
 import com.fs.app.utils.JwtUtils;
@@ -27,8 +26,9 @@ import com.fs.crm.service.*;
 import com.fs.his.domain.FsCity;
 import com.fs.his.service.IFsAppVersionService;
 import com.fs.his.service.IFsCityService;
-import com.fs.his.service.IFsInquiryOrderMsgService;
+import com.fs.his.service.OpenImAsyncService;
 import com.fs.im.dto.OpenImMsgCallBackResponse;
+import com.fs.im.util.JsonUtil;
 import com.fs.im.vo.OpenImMsgCallBackVO;
 import com.fs.jpush.service.JpushService;
 
@@ -40,7 +40,6 @@ import com.fs.system.service.ISysDictDataService;
 import com.fs.system.vo.DictVO;
 import com.fs.voice.service.IVoiceService;
 import com.google.common.collect.Lists;
-import com.google.gson.Gson;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiParam;
@@ -80,8 +79,8 @@ public class CommonController extends AppBaseController {
 	@Autowired
 	private JpushService jpushService;
 
-    @Autowired
-    private IFsInquiryOrderMsgService inquiryOrderMsgService;
+	@Autowired
+	private OpenImAsyncService openImAsyncService;
 	@Autowired
 	private ITencentCloudCosService tencentCloudCosService;
 	public static final Logger LOGGER = LoggerFactory.getLogger(CommonController.class);
@@ -223,15 +222,24 @@ public class CommonController extends AppBaseController {
 
     @ApiOperation("openIm聊天数据回调")
     @PostMapping(value = "/callbackAfterSendSingleMsgCommand")
-    public OpenImMsgCallBackResponse openImMsgCallBack(@RequestBody String body, HttpServletRequest request) throws JsonProcessingException {
-
-        Gson gson = new Gson();
-        OpenImMsgCallBackVO messageInfo = gson.fromJson(body, OpenImMsgCallBackVO.class);
-
-        //openIMService.AiAutoReply(messageInfo);
-
-        log.info("收到的参数{}", JSON.toJSONString(messageInfo));
-        return inquiryOrderMsgService.openImSaveMsg(messageInfo);
+    public OpenImMsgCallBackResponse openImMsgCallBack(@RequestBody String body) {
+        if (StringUtils.isBlank(body)) {
+            log.warn("OpenIM 回调 body 为空");
+            return OpenImMsgCallBackResponse.fail("body为空");
+        }
+        try {
+            OpenImMsgCallBackVO messageInfo = JsonUtil.fromJson(body, OpenImMsgCallBackVO.class);
+            if (messageInfo == null || StringUtils.isBlank(messageInfo.getSendID())) {
+                log.warn("OpenIM 回调参数无效: {}", body);
+                return OpenImMsgCallBackResponse.fail("参数无效");
+            }
+            openImAsyncService.handleCallback(messageInfo);
+            return OpenImMsgCallBackResponse.success();
+        } catch (Exception e) {
+            // 解析失败仍返回成功,避免 OpenIM 反复重试;异常已记录便于排查
+            log.error("OpenIM 回调解析失败, body={}", body, e);
+            return OpenImMsgCallBackResponse.success();
+        }
     }
 
 	/**

+ 14 - 0
fs-company-app/src/main/java/com/fs/core/config/ThreadPoolConfig.java

@@ -43,6 +43,20 @@ public class ThreadPoolConfig
         return executor;
     }
 
+    @Bean(name = "openImExecutor")
+    public ThreadPoolTaskExecutor openImExecutor()
+    {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(10);
+        executor.setMaxPoolSize(50);
+        executor.setQueueCapacity(500);
+        executor.setKeepAliveSeconds(keepAliveSeconds);
+        executor.setThreadNamePrefix("openim-callback-");
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.initialize();
+        return executor;
+    }
+
     /**
      * 执行周期性或定时任务
      */

+ 11 - 1
fs-company/src/main/java/com/fs/company/controller/company/CompanyUserController.java

@@ -848,7 +848,17 @@ public class CompanyUserController extends BaseController {
         OpenImResponseDTO openImResponseDTO = openIMService.importFriend(ownerUserID, friendUserIDs);
         return R.ok().put("data",openImResponseDTO);
     }
-
+    /**
+     * 修改用户 绑定ai角色
+     */
+    @PreAuthorize("@ss.hasPermi('company:user:bindAI')")
+    @Log(title = "用户ai角色管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/updateUserRole")
+    public AjaxResult updateUserRole(@Validated @RequestBody CompanyUser user)
+    {
+        user.setUpdateBy(SecurityUtils.getUsername());
+        return toAjax(companyUserService.updateUserRole(user));
+    }
     /**
      * 根据手机号码精确查询fs_user(完全匹配)
      * @param phone 手机号码

+ 11 - 0
fs-company/src/main/java/com/fs/company/controller/fastGpt/FastGptRoleController.java

@@ -10,6 +10,7 @@ import com.fs.common.utils.ServletUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.fastGpt.domain.FastGptRole;
+import com.fs.fastGpt.param.FastGptRoleNameAndId;
 import com.fs.fastGpt.service.IFastGptRoleService;
 import com.fs.fastGpt.vo.FastGptRoleVO;
 import com.fs.framework.security.LoginUser;
@@ -244,4 +245,14 @@ public class FastGptRoleController extends BaseController
         List<FastGptRole> list = fastGptRoleService.selectFastGptRoleList(role);
         return R.ok().put("data",list);
     }
+
+
+    @GetMapping("/listNameAndId")
+    public R listNameAndId()
+    {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+
+        List<FastGptRoleNameAndId> list = fastGptRoleService.selectFastGptNameAndId(loginUser.getCompany().getCompanyId());
+        return R.ok().put("data",list);
+    }
 }

+ 97 - 0
fs-company/src/main/java/com/fs/company/controller/im/ImChatMsgController.java

@@ -0,0 +1,97 @@
+package com.fs.company.controller.im;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.im.domain.ImChatMsg;
+import com.fs.im.service.IImChatMsgService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 聊天消息Controller
+ *
+ * @author fs
+ * @date 2025-08-18
+ */
+@RestController
+@RequestMapping("/im/msg")
+public class ImChatMsgController extends BaseController
+{
+    @Autowired
+    private IImChatMsgService imChatMsgService;
+
+    /**
+     * 查询聊天消息列表
+     */
+    @PreAuthorize("@ss.hasPermi('im:msg:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(ImChatMsg imChatMsg)
+    {
+        startPage();
+        List<ImChatMsg> list = imChatMsgService.selectImChatMsgList(imChatMsg);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出聊天消息列表
+     */
+    @PreAuthorize("@ss.hasPermi('im:msg:export')")
+    @Log(title = "聊天消息", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(ImChatMsg imChatMsg)
+    {
+        List<ImChatMsg> list = imChatMsgService.selectImChatMsgList(imChatMsg);
+        ExcelUtil<ImChatMsg> util = new ExcelUtil<ImChatMsg>(ImChatMsg.class);
+        return util.exportExcel(list, "聊天消息数据");
+    }
+
+    /**
+     * 获取聊天消息详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('im:msg:query')")
+    @GetMapping(value = "/{msgId}")
+    public AjaxResult getInfo(@PathVariable("msgId") Long msgId)
+    {
+        return AjaxResult.success(imChatMsgService.selectImChatMsgByMsgId(msgId));
+    }
+
+    /**
+     * 新增聊天消息
+     */
+    @PreAuthorize("@ss.hasPermi('im:msg:add')")
+    @Log(title = "聊天消息", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody ImChatMsg imChatMsg)
+    {
+        return toAjax(imChatMsgService.insertImChatMsg(imChatMsg));
+    }
+
+    /**
+     * 修改聊天消息
+     */
+    @PreAuthorize("@ss.hasPermi('im:msg:edit')")
+    @Log(title = "聊天消息", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody ImChatMsg imChatMsg)
+    {
+        return toAjax(imChatMsgService.updateImChatMsg(imChatMsg));
+    }
+
+    /**
+     * 删除聊天消息
+     */
+    @PreAuthorize("@ss.hasPermi('im:msg:remove')")
+    @Log(title = "聊天消息", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{msgIds}")
+    public AjaxResult remove(@PathVariable Long[] msgIds)
+    {
+        return toAjax(imChatMsgService.deleteImChatMsgByMsgIds(msgIds));
+    }
+}

+ 131 - 0
fs-company/src/main/java/com/fs/company/controller/im/ImChatSessionController.java

@@ -0,0 +1,131 @@
+package com.fs.company.controller.im;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.poi.ExcelUtil;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import com.fs.im.domain.ImChatMsg;
+import com.fs.im.domain.ImChatSession;
+import com.fs.im.service.IImChatMsgService;
+import com.fs.im.service.IImChatSessionService;
+import com.fs.im.service.OpenIMService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 聊天会话Controller
+ *
+ * @author fs
+ * @date 2025-08-18
+ */
+@RestController
+@RequestMapping("/im/session")
+public class ImChatSessionController extends BaseController
+{
+    @Autowired
+    private IImChatSessionService imChatSessionService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    @Autowired
+    private IImChatMsgService imChatMsgService;
+
+    @Autowired
+    private OpenIMService openIMService;
+
+
+    /**
+     * 查询对话关系列表
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatSession:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(ImChatSession param)
+    {
+        startPage();
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        param.setCompanyId(loginUser.getCompany().getCompanyId());
+//        param.setKfId(loginUser.getUser().getUserId().toString());
+        List<ImChatSession> list = imChatSessionService.selectImChatSessionList(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出聊天会话列表
+     */
+    @PreAuthorize("@ss.hasPermi('im:session:export')")
+    @Log(title = "聊天会话", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public AjaxResult export(ImChatSession imChatSession)
+    {
+        List<ImChatSession> list = imChatSessionService.selectImChatSessionList(imChatSession);
+        ExcelUtil<ImChatSession> util = new ExcelUtil<ImChatSession>(ImChatSession.class);
+        return util.exportExcel(list, "聊天会话数据");
+    }
+
+    /**
+     * 获取聊天会话详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('fastGpt:fastGptChatSession:query')")
+    @GetMapping(value = "/{sessionId}")
+    public R getInfo(@PathVariable("sessionId") Long sessionId)
+    {
+        ImChatSession imChatSession = imChatSessionService.selectImChatSessionBySessionId(sessionId);
+        ImChatMsg queryParam = new ImChatMsg();
+        queryParam.setSessionId(imChatSession.getSessionId());
+        List<ImChatMsg> imChatMsgs = imChatMsgService.selectImChatMsgList(queryParam);
+        return R.ok().put("data",imChatSession).put("list",imChatMsgs);
+    }
+
+    /**
+     * 新增聊天会话
+     */
+    @PreAuthorize("@ss.hasPermi('im:session:add')")
+    @Log(title = "聊天会话", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody ImChatSession imChatSession)
+    {
+        return toAjax(imChatSessionService.insertImChatSession(imChatSession));
+    }
+
+    /**
+     * 修改聊天会话
+     */
+    @PreAuthorize("@ss.hasPermi('im:session:edit')")
+    @Log(title = "聊天会话", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody ImChatSession imChatSession)
+    {
+        return toAjax(imChatSessionService.updateImChatSession(imChatSession));
+    }
+
+    /**
+     * 删除聊天会话
+     */
+    @PreAuthorize("@ss.hasPermi('im:session:remove')")
+    @Log(title = "聊天会话", businessType = BusinessType.DELETE)
+	@DeleteMapping("/{sessionIds}")
+    public AjaxResult remove(@PathVariable Long[] sessionIds)
+    {
+        return toAjax(imChatSessionService.deleteImChatSessionBySessionIds(sessionIds));
+    }
+    /**
+     * 删除聊天会话
+     */
+	@GetMapping("/updateImSessionToPerson")
+    public AjaxResult updateImSessionToPerson(@RequestParam(value = "chatId") String chatId)
+    {
+        return toAjax(openIMService.cancleAiToPerson(chatId));
+    }
+
+
+}

+ 74 - 0
fs-service/src/main/java/com/fs/app/chat/domain/AppChatHistory.java

@@ -0,0 +1,74 @@
+package com.fs.app.chat.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AppChatHistory {
+
+    /**
+     * 物理主键
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 用户类型,0-用户,1-客服
+     */
+    @Excel(name = "用户类型,0-用户,1-客服")
+    private Integer senderUserType;
+
+    /**
+     * 本次发送账号
+     */
+    @Excel(name = "本次发送账号")
+    private String senderAccount;
+
+    /**
+     * 对话内容
+     */
+    @Excel(name = "对话内容")
+    private String content;
+
+    /**
+     * 用户类型,0-用户,1-客服
+     */
+    @Excel(name = "用户类型,0-用户,1-客服")
+    private Integer receiverUserType;
+
+    /**
+     * 对话内容类型,0-文本,1-图片
+     */
+    @Excel(name = "对话内容类型,1-文本,2-语音,3-图片,4-视频")
+    private Integer contentType;
+
+    /**
+     * 本次接收账号
+     */
+    @Excel(name = "本次接收账号")
+    private String receiverAccount;
+
+    /**
+     * 删除状态,0-未删除,1-已删除
+     */
+    @Excel(name = "删除状态,0-未删除,1-已删除")
+    private Integer isDelete;
+
+    /**
+     * 是否聊天,0-sop/欢迎语等自动发送,1-聊天及回复
+     */
+    private Integer isChat;
+
+    private Long batchId;
+
+    private Date clientSendTime;
+
+    private Date createTime;
+
+    private Date updateTime;
+
+}

+ 60 - 0
fs-service/src/main/java/com/fs/app/chat/domain/AppChatMsg.java

@@ -0,0 +1,60 @@
+package com.fs.app.chat.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AppChatMsg {
+
+    /**
+     * 物理主键
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 会话id
+     */
+    @Excel(name = "会话id")
+    private Long chatSessionId;
+
+    /**
+     * 发送人类型,0-用户,1-客服/AI
+     */
+    @Excel(name = "发送人类型,0-用户,1-客服/AI")
+    private Integer senderType;
+
+    /**
+     * 发送人id,发送人类型是用户时,为用户id,发送人类型是客服/AI时,为客服id
+     */
+    private Long senderId;
+
+    /**
+     * 本次发送的完整内容
+     */
+    @Excel(name = "本次发送的完整内容")
+    private String fullContent;
+
+    /**
+     * 发送时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "发送时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date sendTime;
+
+    /**
+     * 是否删除,0-未删除,1-已删除
+     */
+    @Excel(name = "是否删除,0-未删除,1-已删除")
+    private Integer isDelete;
+
+    private Date createTime;
+
+    private Date updateTime;
+
+}

+ 99 - 0
fs-service/src/main/java/com/fs/app/chat/domain/AppUserChatLogs.java

@@ -0,0 +1,99 @@
+package com.fs.app.chat.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AppUserChatLogs {
+
+    /**
+     * 物理主键
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 批次号
+     */
+    private Long batchId;
+
+    /**
+     * 客户id
+     */
+    @Excel(name = "客户id")
+    private Long userId;
+
+    /**
+     * 客户名称
+     */
+    @Excel(name = "客户名称")
+    private String userName;
+
+    /**
+     * 用户询问简短内容
+     */
+    @Excel(name = "用户询问简短内容")
+    private String userAskShortContent;
+
+    /**
+     * 用户询问完整内容
+     */
+    @Excel(name = "用户询问完整内容")
+    private String userAskFullContent;
+
+    /**
+     * 用户询问时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "用户询问时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date userAskTime;
+
+    /**
+     * 客服id
+     */
+    @Excel(name = "客服id")
+    private Long appCustomerId;
+
+    /**
+     * 客服名称
+     */
+    @Excel(name = "客服名称")
+    private String appCustomerName;
+
+    /**
+     * 客服回复简短内容
+     */
+    @Excel(name = "客服回复简短内容")
+    private String customerReplyShortContent;
+
+    /**
+     * 客服回复完整内容
+     */
+    @Excel(name = "客服回复完整内容")
+    private String customerReplyFullContent;
+
+    /**
+     * 客服回复时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "客服回复时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date customerReplyTime;
+
+    /**
+     * 是否删除,0-未删除,1-已删除
+     */
+    @Excel(name = "是否删除,0-未删除,1-已删除")
+    private Integer isDelete;
+
+    private String extend;
+
+    private Date createTime;
+
+    private Date updateTime;
+
+}

+ 17 - 0
fs-service/src/main/java/com/fs/app/chat/dto/AppUserChatLogsDTO.java

@@ -0,0 +1,17 @@
+package com.fs.app.chat.dto;
+
+import com.fs.app.chat.domain.AppUserChatLogs;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class AppUserChatLogsDTO extends AppUserChatLogs {
+
+    private String userAskDate;
+
+    private String customerReplyDate;
+
+    private String replyContent;
+
+}

+ 64 - 0
fs-service/src/main/java/com/fs/app/chat/mapper/AppChatHistoryMapper.java

@@ -0,0 +1,64 @@
+package com.fs.app.chat.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.app.chat.domain.AppChatHistory;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+/**
+ * app-聊天记录Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-27
+ */
+@Mapper
+public interface AppChatHistoryMapper extends BaseMapper<AppChatHistory> {
+    /**
+     * 查询app-聊天记录
+     *
+     * @param id app-聊天记录主键
+     * @return app-聊天记录
+     */
+    AppChatHistory selectAppChatHistoryById(Long id);
+
+    /**
+     * 查询app-聊天记录列表
+     *
+     * @param appChatHistory app-聊天记录
+     * @return app-聊天记录集合
+     */
+    List<AppChatHistory> selectAppChatHistoryList(AppChatHistory appChatHistory);
+
+    /**
+     * 新增app-聊天记录
+     *
+     * @param appChatHistory app-聊天记录
+     * @return 结果
+     */
+    int insertAppChatHistory(AppChatHistory appChatHistory);
+
+    /**
+     * 修改app-聊天记录
+     *
+     * @param appChatHistory app-聊天记录
+     * @return 结果
+     */
+    int updateAppChatHistory(AppChatHistory appChatHistory);
+
+    /**
+     * 删除app-聊天记录
+     *
+     * @param id app-聊天记录主键
+     * @return 结果
+     */
+    int deleteAppChatHistoryById(Long id);
+
+    /**
+     * 批量删除app-聊天记录
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAppChatHistoryByIds(Long[] ids);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/app/chat/mapper/AppChatMsgMapper.java

@@ -0,0 +1,62 @@
+package com.fs.app.chat.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.app.chat.domain.AppChatMsg;
+
+import java.util.List;
+
+/**
+ * app-用户/AI对话完整记录Mapper接口
+ *
+ * @author fs
+ * @date 2026-04-01
+ */
+public interface AppChatMsgMapper extends BaseMapper<AppChatMsg> {
+    /**
+     * 查询app-用户/AI对话完整记录
+     *
+     * @param id app-用户/AI对话完整记录主键
+     * @return app-用户/AI对话完整记录
+     */
+    AppChatMsg selectAppChatMsgById(Long id);
+
+    /**
+     * 查询app-用户/AI对话完整记录列表
+     *
+     * @param appChatMsg app-用户/AI对话完整记录
+     * @return app-用户/AI对话完整记录集合
+     */
+    List<AppChatMsg> selectAppChatMsgList(AppChatMsg appChatMsg);
+
+    /**
+     * 新增app-用户/AI对话完整记录
+     *
+     * @param appChatMsg app-用户/AI对话完整记录
+     * @return 结果
+     */
+    int insertAppChatMsg(AppChatMsg appChatMsg);
+
+    /**
+     * 修改app-用户/AI对话完整记录
+     *
+     * @param appChatMsg app-用户/AI对话完整记录
+     * @return 结果
+     */
+    int updateAppChatMsg(AppChatMsg appChatMsg);
+
+    /**
+     * 删除app-用户/AI对话完整记录
+     *
+     * @param id app-用户/AI对话完整记录主键
+     * @return 结果
+     */
+    int deleteAppChatMsgById(Long id);
+
+    /**
+     * 批量删除app-用户/AI对话完整记录
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAppChatMsgByIds(Long[] ids);
+}

+ 64 - 0
fs-service/src/main/java/com/fs/app/chat/mapper/AppUserChatLogsMapper.java

@@ -0,0 +1,64 @@
+package com.fs.app.chat.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.app.chat.domain.AppUserChatLogs;
+import com.fs.app.chat.dto.AppUserChatLogsDTO;
+import com.fs.app.chat.vo.AppUserChatLogsVO;
+
+import java.util.List;
+
+/**
+ * app-客户与客服对话(问答)Mapper接口
+ *
+ * @author fs
+ * @date 2026-04-07
+ */
+public interface AppUserChatLogsMapper extends BaseMapper<AppUserChatLogs> {
+    /**
+     * 查询app-客户与客服对话(问答)
+     *
+     * @param id app-客户与客服对话(问答)主键
+     * @return app-客户与客服对话(问答)
+     */
+    AppUserChatLogs selectAppUserChatLogsById(Long id);
+
+    /**
+     * 查询app-客户与客服对话(问答)列表
+     *
+     * @param appUserChatLogs app-客户与客服对话(问答)
+     * @return app-客户与客服对话(问答)集合
+     */
+    List<AppUserChatLogsVO> findList(AppUserChatLogsDTO appUserChatLogs);
+
+    /**
+     * 新增app-客户与客服对话(问答)
+     *
+     * @param appUserChatLogs app-客户与客服对话(问答)
+     * @return 结果
+     */
+    int insertAppUserChatLogs(AppUserChatLogs appUserChatLogs);
+
+    /**
+     * 修改app-客户与客服对话(问答)
+     *
+     * @param appUserChatLogs app-客户与客服对话(问答)
+     * @return 结果
+     */
+    int updateAppUserChatLogs(AppUserChatLogs appUserChatLogs);
+
+    /**
+     * 删除app-客户与客服对话(问答)
+     *
+     * @param id app-客户与客服对话(问答)主键
+     * @return 结果
+     */
+    int deleteAppUserChatLogsById(Long id);
+
+    /**
+     * 批量删除app-客户与客服对话(问答)
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAppUserChatLogsByIds(Long[] ids);
+}

+ 65 - 0
fs-service/src/main/java/com/fs/app/chat/service/IAppChatHistoryService.java

@@ -0,0 +1,65 @@
+package com.fs.app.chat.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.app.chat.domain.AppChatHistory;
+import com.fs.im.vo.OpenImMsgCallBackVO;
+
+import java.util.List;
+
+/**
+ * app-聊天记录Service接口
+ *
+ * @author fs
+ * @date 2026-03-27
+ */
+public interface IAppChatHistoryService extends IService<AppChatHistory> {
+    /**
+     * 查询app-聊天记录
+     *
+     * @param id app-聊天记录主键
+     * @return app-聊天记录
+     */
+    AppChatHistory selectAppChatHistoryById(Long id);
+
+    /**
+     * 查询app-聊天记录列表
+     *
+     * @param appChatHistory app-聊天记录
+     * @return app-聊天记录集合
+     */
+    List<AppChatHistory> selectAppChatHistoryList(AppChatHistory appChatHistory);
+
+    /**
+     * 新增app-聊天记录
+     *
+     * @param appChatHistory app-聊天记录
+     * @return 结果
+     */
+    int insertAppChatHistory(AppChatHistory appChatHistory);
+
+    /**
+     * 修改app-聊天记录
+     *
+     * @param appChatHistory app-聊天记录
+     * @return 结果
+     */
+    int updateAppChatHistory(AppChatHistory appChatHistory);
+
+    /**
+     * 批量删除app-聊天记录
+     *
+     * @param ids 需要删除的app-聊天记录主键集合
+     * @return 结果
+     */
+    int deleteAppChatHistoryByIds(Long[] ids);
+
+    /**
+     * 删除app-聊天记录信息
+     *
+     * @param id app-聊天记录主键
+     * @return 结果
+     */
+    int deleteAppChatHistoryById(Long id);
+
+    void saveChatHistory(OpenImMsgCallBackVO messageInfo);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/app/chat/service/IAppChatMsgService.java

@@ -0,0 +1,62 @@
+package com.fs.app.chat.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.app.chat.domain.AppChatMsg;
+
+import java.util.List;
+
+/**
+ * app-用户/AI对话完整记录Service接口
+ *
+ * @author fs
+ * @date 2026-04-01
+ */
+public interface IAppChatMsgService extends IService<AppChatMsg> {
+    /**
+     * 查询app-用户/AI对话完整记录
+     *
+     * @param id app-用户/AI对话完整记录主键
+     * @return app-用户/AI对话完整记录
+     */
+    AppChatMsg selectAppChatMsgById(Long id);
+
+    /**
+     * 查询app-用户/AI对话完整记录列表
+     *
+     * @param appChatMsg app-用户/AI对话完整记录
+     * @return app-用户/AI对话完整记录集合
+     */
+    List<AppChatMsg> selectAppChatMsgList(AppChatMsg appChatMsg);
+
+    /**
+     * 新增app-用户/AI对话完整记录
+     *
+     * @param appChatMsg app-用户/AI对话完整记录
+     * @return 结果
+     */
+    int insertAppChatMsg(AppChatMsg appChatMsg);
+
+    /**
+     * 修改app-用户/AI对话完整记录
+     *
+     * @param appChatMsg app-用户/AI对话完整记录
+     * @return 结果
+     */
+    int updateAppChatMsg(AppChatMsg appChatMsg);
+
+    /**
+     * 批量删除app-用户/AI对话完整记录
+     *
+     * @param ids 需要删除的app-用户/AI对话完整记录主键集合
+     * @return 结果
+     */
+    int deleteAppChatMsgByIds(Long[] ids);
+
+    /**
+     * 删除app-用户/AI对话完整记录信息
+     *
+     * @param id app-用户/AI对话完整记录主键
+     * @return 结果
+     */
+    int deleteAppChatMsgById(Long id);
+}

+ 272 - 0
fs-service/src/main/java/com/fs/app/chat/service/impl/AppChatHistoryServiceImpl.java

@@ -0,0 +1,272 @@
+package com.fs.app.chat.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.app.chat.domain.AppChatHistory;
+import com.fs.app.chat.mapper.AppChatHistoryMapper;
+import com.fs.app.chat.service.IAppChatHistoryService;
+import com.fs.app.cusrole.domain.AppCustomerRoleMember;
+import com.fs.app.cusrole.service.IAppCustomerRoleMemberService;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.im.dto.OpenImMsgCallBackResponse;
+import com.fs.im.service.IImChatSessionService;
+import com.fs.im.vo.OpenImMsgCallBackVO;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * app-聊天记录Service业务层处理
+ *
+ * @author fs
+ * @date 2026-03-27
+ */
+@Slf4j
+@Service
+public class AppChatHistoryServiceImpl extends ServiceImpl<AppChatHistoryMapper, AppChatHistory> implements IAppChatHistoryService {
+
+    @Autowired
+    private IImChatSessionService imChatSessionService;
+
+    @Autowired
+    private IAppCustomerRoleMemberService appCustomerRoleMemberService;
+
+
+    /**
+     * 查询app-聊天记录
+     *
+     * @param id app-聊天记录主键
+     * @return app-聊天记录
+     */
+    @Override
+    public AppChatHistory selectAppChatHistoryById(Long id) {
+        return baseMapper.selectAppChatHistoryById(id);
+    }
+
+    /**
+     * 查询app-聊天记录列表
+     *
+     * @param appChatHistory app-聊天记录
+     * @return app-聊天记录
+     */
+    @Override
+    public List<AppChatHistory> selectAppChatHistoryList(AppChatHistory appChatHistory) {
+        return baseMapper.selectAppChatHistoryList(appChatHistory);
+    }
+
+    /**
+     * 新增app-聊天记录
+     *
+     * @param appChatHistory app-聊天记录
+     * @return 结果
+     */
+    @Override
+    public int insertAppChatHistory(AppChatHistory appChatHistory) {
+        appChatHistory.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertAppChatHistory(appChatHistory);
+    }
+
+    /**
+     * 修改app-聊天记录
+     *
+     * @param appChatHistory app-聊天记录
+     * @return 结果
+     */
+    @Override
+    public int updateAppChatHistory(AppChatHistory appChatHistory) {
+        appChatHistory.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateAppChatHistory(appChatHistory);
+    }
+
+    /**
+     * 批量删除app-聊天记录
+     *
+     * @param ids 需要删除的app-聊天记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAppChatHistoryByIds(Long[] ids) {
+        return baseMapper.deleteAppChatHistoryByIds(ids);
+    }
+
+    /**
+     * 删除app-聊天记录信息
+     *
+     * @param id app-聊天记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAppChatHistoryById(Long id) {
+        return baseMapper.deleteAppChatHistoryById(id);
+    }
+
+    @Override
+//    @Async
+    public void saveChatHistory(OpenImMsgCallBackVO openImMsgCallBackVO) {
+        OpenImMsgCallBackResponse openImMsgCallBackResponse = new OpenImMsgCallBackResponse();
+        ObjectMapper objectMapper = new ObjectMapper();
+        try {
+            //当前消息的发送人
+            String send = openImMsgCallBackVO.getSendID();
+            //当前消息的接收人
+            String to = openImMsgCallBackVO.getRecvID();
+            //发送时间
+            Long time = openImMsgCallBackVO.getSendTime();
+            if (time == null) {
+                time = System.currentTimeMillis();
+            }
+            Date date = new Date(time);
+            //发送的消息
+            String content = openImMsgCallBackVO.getContent();
+            log.info("收到请求数据:{}", openImMsgCallBackVO);
+            String senderAccount = "";
+            String receiverAccount = "";
+            //用户发送消息
+            if (send.startsWith("U")) {
+                senderAccount = send.replace("U", "");
+                if (!to.startsWith("C")) {
+                    return;
+                }
+                receiverAccount = to.replace("C", "");
+                int ra = Integer.parseInt(receiverAccount);
+                if (!(ra < 2000 || ra > 100000)) {//之前约定的,客服若不是这个范围的不处理
+//                    log.error("当前客服:{},不满足客服条件[客服id<2000 or 客服id>100000],跳过处理!", receiverAccount);
+                    return;
+                }
+            }
+            //客服发送的消息
+            else if (send.startsWith("C")) {
+                senderAccount = send.replace("C", "");
+                int ra = Integer.parseInt(senderAccount);
+                if (!(ra < 2000 || ra > 100000)) {//之前约定的,客服若不是这个范围的不处理
+//                    log.error("当前客服:{},不满足客服条件[客服id<2000 or 客服id>100000],跳过处理!", senderAccount);
+                    return;
+                }
+                if (!to.startsWith("U")) {
+                    return;
+                }
+                receiverAccount = to.replace("U", "");
+            }
+            if (ObjectUtil.isEmpty(senderAccount) || ObjectUtil.isEmpty(receiverAccount)) {
+                log.error("发送人:{},接收人:{}不满足发送条件,跳过处理!", senderAccount, receiverAccount);
+                return;
+            }
+            Integer msgContentType = openImMsgCallBackVO.getContentType();
+            int type = 0;
+            String cont = "";
+            JsonNode jsonNode;
+            openImMsgCallBackVO.setType("im");
+            if (send.startsWith("C") && StringUtils.isEmpty(openImMsgCallBackVO.getSenderNickname())) {
+                AppCustomerRoleMember roleMember = appCustomerRoleMemberService.lambdaQuery()
+                        .eq(AppCustomerRoleMember::getId, Long.parseLong(senderAccount))
+                        .eq(AppCustomerRoleMember::getIsDelete, 0)
+                        .one();
+                openImMsgCallBackVO.setSenderNickname(roleMember.getMemberName());
+            }
+            log.info("发送人:{},接收人:{},发送时间:{},发送内容:{}", senderAccount, receiverAccount, date, cont);
+            if (msgContentType != null) {
+                switch (msgContentType) {
+                    case 1601:
+                        log.info("执行音视频通话推送");
+                        cont = "通话消息";
+                        type = 1;
+                        break;
+                    //普通消息
+                    case 101:
+                        type = 1;
+                        jsonNode = objectMapper.readTree(content);
+                        cont = jsonNode.get("content").asText();
+                        break;
+                    //语音消息
+                    case 103:
+                        jsonNode = objectMapper.readTree(content); // 转为 JsonNode
+                        String soundUrl = jsonNode.get("sourceUrl").asText();
+                        try {
+                            // 创建URL对象
+                            URL url = new URL(soundUrl);
+                            InputStream in = url.openStream();
+                            CloudStorageService storage = OSSFactory.build();
+                            cont = storage.uploadSuffix(in, ".m4a");
+                        } catch (IOException e) {
+                            log.error("语音消息处理异常!", e);
+                        }
+                        type = 2;
+                        break;
+                    //图片消息
+                    case 102:
+                        jsonNode = objectMapper.readTree(content); // 转为 JsonNode
+                        String imgUrl = jsonNode.get("sourcePicture").get("url").asText();
+                        try {
+                            URL url = new URL(imgUrl);
+                            InputStream in = url.openStream();
+                            CloudStorageService storage = OSSFactory.build();
+                            cont = storage.uploadSuffix(in, ".jpg");
+                        } catch (IOException e) {
+                            log.error("图片消息处理异常!", e);
+                        }
+                        type = 3;
+                        break;
+                    //视频消息
+                    case 104:
+                        jsonNode = objectMapper.readTree(content); // 转为 JsonNode
+                        String videoUrl = jsonNode.get("videoUrl").asText();
+                        try {
+                            URL url = new URL(videoUrl);
+                            InputStream in = url.openStream();
+                            CloudStorageService storage = OSSFactory.build();
+                            cont = storage.uploadSuffix(in, "." + jsonNode.get("videoType").asText());
+                        } catch (IOException e) {
+                            log.error("视频消息处理异常!", e);
+                        }
+                        openImMsgCallBackVO.setContent("");
+                        type = 4;
+                        break;
+                    //文件消息
+                    case 105:
+                        jsonNode = objectMapper.readTree(content); // 转为 JsonNode
+                        cont = jsonNode.get("fileName").asText();
+                        type = 4;
+                        openImMsgCallBackVO.setContent("");
+                        break;
+                }
+            }
+            if (StringUtils.isEmpty(cont)) {
+                openImMsgCallBackResponse.setErrMsg("无消息内容,未保存到数据库");
+                return;
+            }
+            AppChatHistory chatHistory = new AppChatHistory();
+            chatHistory.setSenderAccount(senderAccount);
+            chatHistory.setReceiverAccount(receiverAccount);
+            //发送人是用户
+            if (send.startsWith("U")) {
+                chatHistory.setSenderUserType(0);
+                chatHistory.setReceiverUserType(1);
+            }
+            //发送人是客服
+            else if (send.startsWith("C")) {
+                chatHistory.setSenderUserType(1);
+                chatHistory.setReceiverUserType(0);
+            }
+            chatHistory.setContentType(type);
+            chatHistory.setContent(cont);
+            chatHistory.setClientSendTime(date);
+            chatHistory.setCreateTime(new Date());
+            chatHistory.setIsDelete(0);
+            chatHistory.setBatchId(time);
+            this.save(chatHistory);
+        } catch (Exception e) {
+            log.error("同步失败", e);
+        }
+    }
+}

+ 88 - 0
fs-service/src/main/java/com/fs/app/chat/service/impl/AppChatMsgServiceImpl.java

@@ -0,0 +1,88 @@
+package com.fs.app.chat.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.app.chat.domain.AppChatMsg;
+import com.fs.app.chat.mapper.AppChatMsgMapper;
+import com.fs.app.chat.service.IAppChatMsgService;
+import com.fs.common.utils.DateUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * app-用户/AI对话完整记录Service业务层处理
+ *
+ * @author fs
+ * @date 2026-04-01
+ */
+@Service
+public class AppChatMsgServiceImpl extends ServiceImpl<AppChatMsgMapper, AppChatMsg> implements IAppChatMsgService {
+
+    /**
+     * 查询app-用户/AI对话完整记录
+     *
+     * @param id app-用户/AI对话完整记录主键
+     * @return app-用户/AI对话完整记录
+     */
+    @Override
+    public AppChatMsg selectAppChatMsgById(Long id) {
+        return baseMapper.selectAppChatMsgById(id);
+    }
+
+    /**
+     * 查询app-用户/AI对话完整记录列表
+     *
+     * @param appChatMsg app-用户/AI对话完整记录
+     * @return app-用户/AI对话完整记录
+     */
+    @Override
+    public List<AppChatMsg> selectAppChatMsgList(AppChatMsg appChatMsg) {
+        return baseMapper.selectAppChatMsgList(appChatMsg);
+    }
+
+    /**
+     * 新增app-用户/AI对话完整记录
+     *
+     * @param appChatMsg app-用户/AI对话完整记录
+     * @return 结果
+     */
+    @Override
+    public int insertAppChatMsg(AppChatMsg appChatMsg) {
+        appChatMsg.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertAppChatMsg(appChatMsg);
+    }
+
+    /**
+     * 修改app-用户/AI对话完整记录
+     *
+     * @param appChatMsg app-用户/AI对话完整记录
+     * @return 结果
+     */
+    @Override
+    public int updateAppChatMsg(AppChatMsg appChatMsg) {
+        appChatMsg.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateAppChatMsg(appChatMsg);
+    }
+
+    /**
+     * 批量删除app-用户/AI对话完整记录
+     *
+     * @param ids 需要删除的app-用户/AI对话完整记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAppChatMsgByIds(Long[] ids) {
+        return baseMapper.deleteAppChatMsgByIds(ids);
+    }
+
+    /**
+     * 删除app-用户/AI对话完整记录信息
+     *
+     * @param id app-用户/AI对话完整记录主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAppChatMsgById(Long id) {
+        return baseMapper.deleteAppChatMsgById(id);
+    }
+}

+ 11 - 0
fs-service/src/main/java/com/fs/app/chat/vo/AppUserChatLogsVO.java

@@ -0,0 +1,11 @@
+package com.fs.app.chat.vo;
+
+import com.fs.app.chat.domain.AppUserChatLogs;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class AppUserChatLogsVO extends AppUserChatLogs {
+
+}

+ 33 - 0
fs-service/src/main/java/com/fs/app/cusrole/domain/AppCustomerRole.java

@@ -0,0 +1,33 @@
+package com.fs.app.cusrole.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("app_customer_role")
+public class AppCustomerRole {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String roleName;
+
+    private String remark;
+
+    private Integer isDelete;
+
+    private String avatar;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+
+    private Long appFastgptRoleId;
+
+    private Integer memberMaxFriendCount;
+
+}

+ 72 - 0
fs-service/src/main/java/com/fs/app/cusrole/domain/AppCustomerRoleMember.java

@@ -0,0 +1,72 @@
+package com.fs.app.cusrole.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AppCustomerRoleMember {
+
+    /**
+     * 物理主键:也是组内具体客服id,默认的一条等于客服组的id,根据客服组扩容策略,每次累加1
+     * 客服组扩容策略:由于一个客服只能添加5000(后续也可能调整)个用户,
+     * 但是要对于客户来说是无感的,所以添加客户是,除了id不一样,其余头像,名称等都一样,
+     * 都延用客服组的信息,这里为了方便,会把组的信息同步给每个组员,
+     * 也为了以后可能每个组员需要区别化配置,把名称,头像等信息字段都同步
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 客服组id
+     */
+    @Excel(name = "客服组id")
+    private Long appCustomerId;
+
+    /**
+     * 组成员名称
+     */
+    @Excel(name = "组成员名称")
+    private String memberName;
+
+    /**
+     * 组内成员编号,每个组都是从1开始递增的
+     */
+    private Long memberNo;
+
+    /**
+     * 头像
+     */
+    @Excel(name = "头像")
+    private String avatar;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 对应ai的配置信息主键
+     */
+    @Excel(name = "对应ai的配置信息主键")
+    private Long appFastgptRoleId;
+
+    /**
+     * 是否删除,0-未删除,1-已删除
+     */
+    @Excel(name = "是否删除,0-未删除,1-已删除")
+    private Integer isDelete;
+
+    private Date createTime;
+
+    private Date updateTime;
+
+    //客服组内每个成员最大添加客户数
+    @TableField(exist = false)
+    private Integer memberMaxFriendCount;
+
+}

+ 98 - 0
fs-service/src/main/java/com/fs/app/cusrole/domain/AppFastGptRole.java

@@ -0,0 +1,98 @@
+package com.fs.app.cusrole.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+import java.sql.Time;
+
+/**
+ * 应用对象 fastgpt_role
+ *
+ * @author fs
+ * @date 2024-09-30
+ */
+@Data
+public class AppFastGptRole extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** ID */
+    private Long roleId;
+
+    /** 角色名 */
+    @Excel(name = "角色名")
+    private String roleName;
+
+    /** 所属公司 */
+    @Excel(name = "所属公司")
+    private Long companyId;
+
+    /** 角色类型 1 AI客服 */
+    @Excel(name = "角色类型 1 AI客服")
+    private Integer roleType;
+
+    //模型策略
+    private String modelType;
+
+    /** 模型JSON */
+    @Excel(name = "模型JSON")
+    private String modeConfigJson;
+
+    @Excel(name = "群聊模型JSON")
+    private String groupModeConfigJson;
+
+    /** 模型 1 fastGpt */
+    @Excel(name = "模型 1 fastGpt")
+    private Integer mode;
+
+    /** 客服ID */
+    @Excel(name = "客服ID")
+    private String kfId;
+
+    /** 客服应用URL */
+    @Excel(name = "客服应用URL")
+    private String kfUrl;
+
+    /** 客服应用头像 */
+    @Excel(name = "客服应用头像")
+    private String avatar;
+
+    /** $column.columnComment */
+    @Excel(name = "客服应用头像")
+    private String kfMediaId;
+    /** 提示词 */
+    private String reminderWords;
+    /**
+    * 绑定的公司
+    */
+    private String bindCorpId;
+
+    private String contactInfo;
+
+    private String channelType;
+
+    private Integer logistics;
+
+    //回复禁止起始时间
+    private Time forbidSendStart;
+
+    //回复禁止结束时间
+    private Time forbidSendEnd;
+
+    /**
+     * 是否禁止时段回复 0是不开启禁止  1是开启禁止 默认为1
+     */
+    private Integer forbidStatus;
+
+    /**
+     * 是否回复语音  0是不回复语音 1是回复语音 默认为1
+     */
+    private Integer sendVoiceStatus;
+
+    /**
+     * 是否发送事件物流信息 0是不发送 1是发送 默认为1
+     */
+    private Integer eventLogistics;
+
+}

+ 46 - 0
fs-service/src/main/java/com/fs/app/cusrole/domain/AppUserCustomerFriendship.java

@@ -0,0 +1,46 @@
+package com.fs.app.cusrole.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AppUserCustomerFriendship {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 客服组id
+     */
+    private Long appCustomerGroupId;
+
+    /**
+     * 客服组实际成员id(真实发送时的客服id,
+     * 即便解绑,此条关联数据也不清理,避免后续重复绑定后,
+     * 发送人不一致出现多对话框)
+     */
+    private Long appCustomerMemberId;
+
+    /**
+     * 用户id
+     */
+    private Long userId;
+
+    /**
+     * 首次绑定时间
+     */
+    private Date firstBindTime;
+
+    /**
+     * 是否删除,0-未删除,1-已删除
+     */
+    private Integer isDelete;
+
+    private Date createTime;
+
+    private Date updateTime;
+
+}

+ 20 - 0
fs-service/src/main/java/com/fs/app/cusrole/dto/AppCustomerRoleDTO.java

@@ -0,0 +1,20 @@
+package com.fs.app.cusrole.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class AppCustomerRoleDTO {
+
+    private Long id;
+
+    private String roleName;
+
+    private String remark;
+
+    private List<String> ids;
+
+    private List<Long> ignoreIds;
+
+}

+ 77 - 0
fs-service/src/main/java/com/fs/app/cusrole/mapper/AppCustomerRoleMemberMapper.java

@@ -0,0 +1,77 @@
+package com.fs.app.cusrole.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.app.cusrole.domain.AppCustomerRoleMember;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * app-客服组-组员信息Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-19
+ */
+@Mapper
+public interface AppCustomerRoleMemberMapper extends BaseMapper<AppCustomerRoleMember> {
+    /**
+     * 查询app-客服组-组员信息
+     *
+     * @param id app-客服组-组员信息主键
+     * @return app-客服组-组员信息
+     */
+    AppCustomerRoleMember selectAppCustomerRoleMemberById(Long id);
+
+    /**
+     * 查询app-客服组-组员信息列表
+     *
+     * @param appCustomerRoleMember app-客服组-组员信息
+     * @return app-客服组-组员信息集合
+     */
+    List<AppCustomerRoleMember> selectAppCustomerRoleMemberList(AppCustomerRoleMember appCustomerRoleMember);
+
+    /**
+     * 新增app-客服组-组员信息
+     *
+     * @param appCustomerRoleMember app-客服组-组员信息
+     * @return 结果
+     */
+    int insertAppCustomerRoleMember(AppCustomerRoleMember appCustomerRoleMember);
+
+    /**
+     * 修改app-客服组-组员信息
+     *
+     * @param appCustomerRoleMember app-客服组-组员信息
+     * @return 结果
+     */
+    int updateAppCustomerRoleMember(AppCustomerRoleMember appCustomerRoleMember);
+
+    /**
+     * 删除app-客服组-组员信息
+     *
+     * @param id app-客服组-组员信息主键
+     * @return 结果
+     */
+    int deleteAppCustomerRoleMemberById(Long id);
+
+    /**
+     * 批量删除app-客服组-组员信息
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteAppCustomerRoleMemberByIds(Long[] ids);
+
+    /**
+     *
+     * @param customerGroupId 客服组id
+     */
+    AppCustomerRoleMember getMaxByCustomerGroupId(@Param("customerGroupId") Long customerGroupId);
+
+    /**
+     *
+     * @param customerGroupId 客服组id
+     */
+    List<AppCustomerRoleMember> getMembersByCustomerGroupId(@Param("customerGroupId") Long customerGroupId);
+}

+ 13 - 0
fs-service/src/main/java/com/fs/app/cusrole/mapper/AppFastGptRoleMapper.java

@@ -0,0 +1,13 @@
+package com.fs.app.cusrole.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.app.cusrole.domain.AppFastGptRole;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+@Mapper
+public interface AppFastGptRoleMapper extends BaseMapper<AppFastGptRole> {
+
+
+    AppFastGptRole selectAppFastGptRoleById(@Param("appFastGptRoleId") Long appFastGptRoleId);
+}

+ 12 - 0
fs-service/src/main/java/com/fs/app/cusrole/mapper/AppUserCustomerFriendshipMapper.java

@@ -0,0 +1,12 @@
+package com.fs.app.cusrole.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.app.cusrole.domain.AppUserCustomerFriendship;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AppUserCustomerFriendshipMapper extends BaseMapper<AppUserCustomerFriendship> {
+
+    int bindFriendship(AppUserCustomerFriendship friendship);
+
+}

+ 69 - 0
fs-service/src/main/java/com/fs/app/cusrole/service/IAppCustomerRoleMemberService.java

@@ -0,0 +1,69 @@
+package com.fs.app.cusrole.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.app.cusrole.domain.AppCustomerRoleMember;
+
+import java.util.List;
+
+/**
+ * app-客服组-组员信息Service接口
+ *
+ * @author fs
+ * @date 2026-03-19
+ */
+public interface IAppCustomerRoleMemberService extends IService<AppCustomerRoleMember> {
+    /**
+     * 查询app-客服组-组员信息
+     *
+     * @param id app-客服组-组员信息主键
+     * @return app-客服组-组员信息
+     */
+    AppCustomerRoleMember selectAppCustomerRoleMemberById(Long id);
+
+    /**
+     * 查询app-客服组-组员信息列表
+     *
+     * @param appCustomerRoleMember app-客服组-组员信息
+     * @return app-客服组-组员信息集合
+     */
+    List<AppCustomerRoleMember> selectAppCustomerRoleMemberList(AppCustomerRoleMember appCustomerRoleMember);
+
+    /**
+     * 新增app-客服组-组员信息
+     *
+     * @param appCustomerRoleMember app-客服组-组员信息
+     * @return 结果
+     */
+    int insertAppCustomerRoleMember(AppCustomerRoleMember appCustomerRoleMember);
+
+    /**
+     * 修改app-客服组-组员信息
+     *
+     * @param appCustomerRoleMember app-客服组-组员信息
+     * @return 结果
+     */
+    int updateAppCustomerRoleMember(AppCustomerRoleMember appCustomerRoleMember);
+
+    /**
+     * 批量删除app-客服组-组员信息
+     *
+     * @param ids 需要删除的app-客服组-组员信息主键集合
+     * @return 结果
+     */
+    int deleteAppCustomerRoleMemberByIds(Long[] ids);
+
+    /**
+     * 删除app-客服组-组员信息信息
+     *
+     * @param id app-客服组-组员信息主键
+     * @return 结果
+     */
+    int deleteAppCustomerRoleMemberById(Long id);
+
+    /**
+     * 获取当前客服组内最大的客服成员
+     *
+     * @param customerGroupId 客服组id
+     */
+    AppCustomerRoleMember getMaxByCustomerGroupId(Long customerGroupId);
+}

+ 9 - 0
fs-service/src/main/java/com/fs/app/cusrole/service/IAppFastGptRoleService.java

@@ -0,0 +1,9 @@
+package com.fs.app.cusrole.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.app.cusrole.domain.AppFastGptRole;
+
+public interface IAppFastGptRoleService  extends IService<AppFastGptRole> {
+
+    AppFastGptRole selectAppFastGptRoleById(Long appFastGptRoleId);
+}

+ 7 - 0
fs-service/src/main/java/com/fs/app/cusrole/service/IAppUserCustomerFriendshipService.java

@@ -0,0 +1,7 @@
+package com.fs.app.cusrole.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.app.cusrole.domain.AppUserCustomerFriendship;
+
+public interface IAppUserCustomerFriendshipService extends IService<AppUserCustomerFriendship> {
+}

+ 93 - 0
fs-service/src/main/java/com/fs/app/cusrole/service/impl/AppCustomerRoleMemberServiceImpl.java

@@ -0,0 +1,93 @@
+package com.fs.app.cusrole.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.app.cusrole.domain.AppCustomerRoleMember;
+import com.fs.app.cusrole.mapper.AppCustomerRoleMemberMapper;
+import com.fs.app.cusrole.service.IAppCustomerRoleMemberService;
+import com.fs.common.utils.DateUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * app-客服组-组员信息Service业务层处理
+ *
+ * @author fs
+ * @date 2026-03-19
+ */
+@Service
+public class AppCustomerRoleMemberServiceImpl extends ServiceImpl<AppCustomerRoleMemberMapper, AppCustomerRoleMember> implements IAppCustomerRoleMemberService {
+
+    /**
+     * 查询app-客服组-组员信息
+     *
+     * @param id app-客服组-组员信息主键
+     * @return app-客服组-组员信息
+     */
+    @Override
+    public AppCustomerRoleMember selectAppCustomerRoleMemberById(Long id) {
+        return baseMapper.selectAppCustomerRoleMemberById(id);
+    }
+
+    /**
+     * 查询app-客服组-组员信息列表
+     *
+     * @param appCustomerRoleMember app-客服组-组员信息
+     * @return app-客服组-组员信息
+     */
+    @Override
+    public List<AppCustomerRoleMember> selectAppCustomerRoleMemberList(AppCustomerRoleMember appCustomerRoleMember) {
+        return baseMapper.selectAppCustomerRoleMemberList(appCustomerRoleMember);
+    }
+
+    /**
+     * 新增app-客服组-组员信息
+     *
+     * @param appCustomerRoleMember app-客服组-组员信息
+     * @return 结果
+     */
+    @Override
+    public int insertAppCustomerRoleMember(AppCustomerRoleMember appCustomerRoleMember) {
+        appCustomerRoleMember.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertAppCustomerRoleMember(appCustomerRoleMember);
+    }
+
+    /**
+     * 修改app-客服组-组员信息
+     *
+     * @param appCustomerRoleMember app-客服组-组员信息
+     * @return 结果
+     */
+    @Override
+    public int updateAppCustomerRoleMember(AppCustomerRoleMember appCustomerRoleMember) {
+        appCustomerRoleMember.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateAppCustomerRoleMember(appCustomerRoleMember);
+    }
+
+    /**
+     * 批量删除app-客服组-组员信息
+     *
+     * @param ids 需要删除的app-客服组-组员信息主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAppCustomerRoleMemberByIds(Long[] ids) {
+        return baseMapper.deleteAppCustomerRoleMemberByIds(ids);
+    }
+
+    /**
+     * 删除app-客服组-组员信息信息
+     *
+     * @param id app-客服组-组员信息主键
+     * @return 结果
+     */
+    @Override
+    public int deleteAppCustomerRoleMemberById(Long id) {
+        return baseMapper.deleteAppCustomerRoleMemberById(id);
+    }
+
+    @Override
+    public AppCustomerRoleMember getMaxByCustomerGroupId(Long customerGroupId) {
+        return baseMapper.getMaxByCustomerGroupId(customerGroupId);
+    }
+}

+ 17 - 0
fs-service/src/main/java/com/fs/app/cusrole/service/impl/AppFastGptRoleImpl.java

@@ -0,0 +1,17 @@
+package com.fs.app.cusrole.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.app.cusrole.domain.AppFastGptRole;
+import com.fs.app.cusrole.mapper.AppFastGptRoleMapper;
+import com.fs.app.cusrole.service.IAppFastGptRoleService;
+import org.springframework.stereotype.Service;
+
+
+@Service
+public class AppFastGptRoleImpl extends ServiceImpl<AppFastGptRoleMapper, AppFastGptRole> implements IAppFastGptRoleService {
+
+    @Override
+    public AppFastGptRole selectAppFastGptRoleById(Long appFastGptRoleId) {
+        return baseMapper.selectAppFastGptRoleById(appFastGptRoleId);
+    }
+}

+ 11 - 0
fs-service/src/main/java/com/fs/app/cusrole/service/impl/AppUserCustomerFriendshipServiceImpl.java

@@ -0,0 +1,11 @@
+package com.fs.app.cusrole.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.app.cusrole.domain.AppUserCustomerFriendship;
+import com.fs.app.cusrole.mapper.AppUserCustomerFriendshipMapper;
+import com.fs.app.cusrole.service.IAppUserCustomerFriendshipService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AppUserCustomerFriendshipServiceImpl extends ServiceImpl<AppUserCustomerFriendshipMapper, AppUserCustomerFriendship> implements IAppUserCustomerFriendshipService {
+}

+ 18 - 0
fs-service/src/main/java/com/fs/app/cusrole/vo/AppCustomerRoleVO.java

@@ -0,0 +1,18 @@
+package com.fs.app.cusrole.vo;
+
+import lombok.Data;
+
+@Data
+public class AppCustomerRoleVO {
+
+    private Long id;
+
+    private String roleName;
+
+    private String remark;
+
+    private String avatar;
+
+    private Long appFastgptRoleId;
+
+}

+ 8 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyUser.java

@@ -62,6 +62,14 @@ public class CompanyUser extends BaseEntity
     /** 手机号码 */
     @Excel(name = "手机号码")
     private String phonenumber;
+
+    private Long fastgptRoleId;
+
+    private Long aiCallUserId;
+
+    private Long noticeQwUserId;
+
+    private Long aiSipCallUserId;
     /** 岗位 */
     private List<CompanyPost> posts;
 

+ 2 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyUserMapper.java

@@ -367,4 +367,6 @@ public interface CompanyUserMapper
     VcCompanyUser selectVcCompanyUserByCompanyUserId(@Param("companyUserId")Long companyUserId);
 
     int updateVcCompanyUser(@Param("vcCompanyUser") VcCompanyUser vcCompanyUser);
+    @Update("update company_user set fastgpt_role_id=#{user.fastgptRoleId} where user_id=#{user.userId}")
+    int updateUserRole(@Param("user") CompanyUser user);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyUserService.java

@@ -313,4 +313,6 @@ public interface ICompanyUserService {
      * 获取销售绑定的fs_user
      */
     int countCompanyUserByUserId(Long userId);
+
+    int updateUserRole(CompanyUser user);
 }

+ 7 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyUserServiceImpl.java

@@ -1258,5 +1258,12 @@ public class CompanyUserServiceImpl implements ICompanyUserService
         // 格式化为指定位数
         return String.format("%0" + length + "d", transformed);
     }
+    @Override
+    public int updateUserRole(CompanyUser user) {
+        if (user.getUserId() == null) {
+            return 0;
+        }
+        return companyUserMapper.updateUserRole(user);
+    }
 
 }

+ 15 - 0
fs-service/src/main/java/com/fs/fastGpt/domain/FastGptChatConversation.java

@@ -0,0 +1,15 @@
+package com.fs.fastGpt.domain;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import lombok.Data;
+
+@Data
+public class FastGptChatConversation {
+    private JSONObject userInfo;
+    private String aiInfo;
+    private JSONArray history;
+    private String isRepository;
+    private String userContent;
+    private String aiContent;
+}

+ 5 - 0
fs-service/src/main/java/com/fs/fastGpt/domain/FastGptRole.java

@@ -84,4 +84,9 @@ public class FastGptRole extends BaseEntity
 
     //课程Id
     private Long courseId;
+
+    /**
+     * 需要获取的客户信息
+     */
+    private String userInfo;
 }

+ 5 - 1
fs-service/src/main/java/com/fs/fastGpt/mapper/FastGptRoleMapper.java

@@ -4,6 +4,7 @@ import java.util.List;
 
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.fastGpt.domain.FastGptRole;
+import com.fs.fastGpt.param.FastGptRoleNameAndId;
 import com.fs.fastGpt.vo.FastGptRoleVO;
 import com.fs.fastGpt.vo.FastgptEventLogTotalVo;
 import com.fs.his.vo.OptionsVO;
@@ -90,7 +91,7 @@ public interface FastGptRoleMapper
 
     @Select("select id dictValue,name dictLabel from fastgpt_role_type ")
     List<OptionsVO> selectFastGptRoleType();
-    @Select("select r.role_id, r.role_name,t.contact_info,r.company_id, r.create_time, r.update_time, r.role_type, r.mode_config_json, r.mode, r.kf_id, r.kf_url, r.avatar, r.kf_media_id,r.reminder_words, r.bind_corp_id,r.channel_type,r.send_course_status,r.course_id from fastgpt_role r LEFT JOIN fastgpt_role_type t on t.id =r.role_type where role_id = #{roleId}")
+    @Select("select r.role_id, r.role_name, t.contact_info, r.user_info, r.company_id, r.create_time, r.update_time, r.role_type, r.mode_config_json, r.mode, r.kf_id, r.kf_url, r.avatar, r.kf_media_id, r.reminder_words, r.bind_corp_id, r.channel_type, r.send_course_status, r.course_id from fastgpt_role r LEFT JOIN fastgpt_role_type t on t.id = r.role_type where r.role_id = #{roleId}")
     FastGptRole selectFastGptRoleTypeByRoleId(Long roleId);
 
     List<FastGptRole> selectFastGptRoleByRoleIds(@Param("roleIds") List<Long> roleIds);
@@ -118,4 +119,7 @@ public interface FastGptRoleMapper
     List<FastGptRoleVO> selectFastGptRoleListVONew(FastGptRole fastGptRole);
 
     List<FsUserCourseVideo> selectAllCourseList(@Param("companyId") Long companyId);
+
+    @Select("select role_id,role_name from fastgpt_role where company_id=#{companyId}")
+    List<FastGptRoleNameAndId> selectFastGptNameAndId(@Param("companyId") Long companyId);
 }

+ 26 - 0
fs-service/src/main/java/com/fs/fastGpt/param/FastGptRoleNameAndId.java

@@ -0,0 +1,26 @@
+package com.fs.fastGpt.param;
+
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 应用对象 fastgpt_role
+ *
+ * @author fs
+ * @date 2024-09-30
+ */
+@Data
+public class FastGptRoleNameAndId implements Serializable
+{
+    private static final long serialVersionUID = 1L;
+
+    /** ID */
+    private Long roleId;
+
+    /** 角色名 */
+    @Excel(name = "角色名")
+    private String roleName;
+
+}

+ 6 - 2
fs-service/src/main/java/com/fs/fastGpt/service/AiHookService.java

@@ -1,6 +1,8 @@
 package com.fs.fastGpt.service;
 
 import com.fs.common.core.domain.R;
+import com.fs.company.domain.CompanyUser;
+import com.fs.his.domain.FsUser;
 import com.fs.im.vo.OpenImMsgCallBackVO;
 import com.fs.qwHookApi.vo.QwHookVO;
 import com.fs.wxwork.dto.WxWorkResponseDTO;
@@ -15,8 +17,10 @@ public interface AiHookService {
     /** 转人工 **/
     void artificial(QwHookVO vo);
 
-    /** ai自动回复 **/
-    R AiReply(OpenImMsgCallBackVO openImMsgDTO, Long companyId);
+
+    /** ai自动回复 (IM渠道 - FsUser) **/
+    R AiReply(OpenImMsgCallBackVO openImMsgDTO, FsUser fsUser);
+
 
     R qwHookNotifyAddMsg(Long qwUserID, Long sender,String count,String uid);
 

+ 3 - 0
fs-service/src/main/java/com/fs/fastGpt/service/IFastGptRoleService.java

@@ -5,6 +5,7 @@ import java.util.List;
 import com.fs.common.core.domain.R;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.fastGpt.domain.FastGptRole;
+import com.fs.fastGpt.param.FastGptRoleNameAndId;
 import com.fs.fastGpt.vo.FastGptRoleDataVO;
 import com.fs.fastGpt.vo.FastGptRoleVO;
 import com.fs.his.vo.OptionsVO;
@@ -85,4 +86,6 @@ public interface IFastGptRoleService
     List<FastGptRoleVO> selectFastGptRoleListVONew(FastGptRole fastGptRole);
 
     List<FsUserCourseVideo> selectAllCourseList(Long companyId);
+
+    List<FastGptRoleNameAndId> selectFastGptNameAndId(Long companyId);
 }

+ 882 - 2
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -4,12 +4,15 @@ import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.common.annotation.Excel;
 import com.fs.common.config.FSConfig;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.company.domain.CompanyConfig;
+import com.fs.company.domain.CompanyUser;
 import com.fs.company.mapper.CompanyConfigMapper;
+import com.fs.company.mapper.CompanyUserMapper;
 import com.fs.config.ai.AiHostProper;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.course.mapper.FsCourseWatchLogMapper;
@@ -26,6 +29,7 @@ import com.fs.fastGpt.mapper.FastGptChatSessionMapper;
 import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
 import com.fs.fastGpt.param.SendAIParam;
 import com.fs.fastGpt.service.*;
+import com.fs.fastGpt.utils.TencentAudioUtils;
 import com.fs.fastgptApi.param.ChatParam;
 import com.fs.fastgptApi.result.ChatDetailTStreamFResult;
 import com.fs.fastgptApi.result.KnowledgeBaseResult;
@@ -34,15 +38,27 @@ import com.fs.fastgptApi.service.Impl.AudioServiceImpl;
 import com.fs.fastgptApi.util.AiImgUtil;
 import com.fs.fastgptApi.util.EventLogUtils;
 import com.fs.fastgptApi.vo.AudioVO;
+import com.fs.his.domain.FsInquiryOrderMsg;
 import com.fs.his.domain.FsStoreOrder;
+import com.fs.his.domain.FsUser;
 import com.fs.his.dto.ExpressInfoDTO;
+import com.fs.his.dto.PayloadDTO;
 import com.fs.his.dto.TracesDTO;
 import com.fs.his.enums.ShipperCodeEnum;
 import com.fs.his.mapper.FsStoreMapper;
 import com.fs.his.mapper.FsStoreOrderMapper;
+import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsExpressService;
+import com.fs.his.service.IFsInquiryOrderMsgService;
 import com.fs.his.service.IFsStoreOrderService;
+import com.fs.im.config.IMConfig;
+import com.fs.im.domain.ImChatMsg;
+import com.fs.im.domain.ImChatSession;
 import com.fs.im.dto.OpenImMsgDTO;
+import com.fs.im.dto.OpenImResponseDTO;
+import com.fs.im.service.IImChatMsgService;
+import com.fs.im.service.IImChatSessionService;
+import com.fs.im.util.OpenIMUtil;
 import com.fs.im.vo.OpenImMsgCallBackVO;
 import com.fs.qw.domain.*;
 import com.fs.qw.mapper.*;
@@ -61,6 +77,7 @@ import com.fs.utils.SensitiveDataUtils;
 import com.fs.voice.utils.StringUtil;
 import com.fs.wxwork.dto.*;
 import com.fs.wxwork.service.WxWorkService;
+import com.google.gson.Gson;
 import com.vdurmont.emoji.EmojiParser;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.ObjectUtils;
@@ -86,10 +103,25 @@ import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import static com.fs.his.utils.PhoneUtil.decryptPhone;
+import static net.sf.jsqlparser.util.validation.metadata.NamedObject.user;
 
 @Slf4j
 @Service
 public class AiHookServiceImpl implements AiHookService {
+
+    /** OpenIM 内容类型:图片 */
+    private static final int IM_CONTENT_IMAGE = 102;
+    /** OpenIM 内容类型:语音 */
+    private static final int IM_CONTENT_VOICE = 103;
+    /** OpenIM 内容类型:视频 */
+    private static final int IM_CONTENT_VIDEO = 104;
+    /** IM AI 回复 Redis 缓存时长(分钟),与 sendFsUserIMAiMsg 内 5s 等待配合实现防抖 */
+    private static final int IM_REPLY_CACHE_MINUTES = 5;
+    /** sendFsUserIMAiMsg 内校验用的 reply 槽位,仅 slot 匹配时才继续请求 FastGPT */
+    private static final int IM_REPLY_SLOT = 1;
+    private static final String IM_REPLY_CACHE_KEY = "IM:reply:";
+    private static final String IM_MSG_CACHE_KEY = "IM:msg:";
+
     @Autowired
     private FastGptChatSessionMapper fastGptChatSessionMapper;
     @Autowired
@@ -97,6 +129,12 @@ public class AiHookServiceImpl implements AiHookService {
     @Autowired
     private QwUserMapper qwUserMapper;
     @Autowired
+    private IFsInquiryOrderMsgService fsInquiryOrderMsgService;
+    @Autowired
+    IMConfig imConfig;
+    @Autowired
+    private IImChatMsgService imChatMsgService;
+    @Autowired
     private IFastGptRoleService roleService;
     @Autowired
     private QwExternalContactInfoMapper qwExternalContactInfoMapper;
@@ -107,6 +145,8 @@ public class AiHookServiceImpl implements AiHookService {
     @Autowired
     QwApiService qwApiService;
     @Autowired
+    private IImChatSessionService imChatSessionService;
+    @Autowired
     QwCompanyMapper qwCompanyMapper;
 
     @Autowired
@@ -168,6 +208,10 @@ public class AiHookServiceImpl implements AiHookService {
     private IFastGptChatReplaceTextService fastGptChatReplaceTextService;
     @Autowired
     private ICrmMsgService crmMsgService;
+    @Autowired
+    private FsUserMapper fsUserMapper;
+    @Autowired
+    private CompanyUserMapper companyUserMapper;
 
     private static final String AI_REPLY = "AI_REPLY:";
     private static final String AI_REPLY_TAG = "AI_REPLY_TAG:";
@@ -330,8 +374,844 @@ public class AiHookServiceImpl implements AiHookService {
         }
         return R.ok();
     }
-
+    /**
+     * ai自动回复 (IM渠道 - 基于FsUser)
+     * 适用场景:没有qwUser/qwExternalContact,只有fsUser信息的IM回调
+     * 转人工时通过IM发送消息给销售
+     */
     @Override
+    public R AiReply(OpenImMsgCallBackVO openImMsgDTO, FsUser fsUser) {
+        if (fsUser == null) {
+            return R.error("用户信息为空");
+        }
+
+        CompanyUser companyUser = resolveCompanyUserForFsUser(fsUser);
+        if (companyUser == null) {
+            log.error("AiReply(FsUser): 未找到关联的销售, fsUserId={}", fsUser.getUserId());
+            return R.error("未找到关联的销售");
+        }
+        if (companyUser.getFastgptRoleId() == null) {
+            return R.error("销售未绑定AI角色,请先绑定角色");
+        }
+
+        FastGptRole role = roleService.selectFastGptRoleTypeByRoleId(companyUser.getFastgptRoleId());
+        ModeConfig config = resolveModeConfig(role);
+        if (config == null) {
+            return null;
+        }
+
+        ImContentParseResult parseResult = parseImMultimediaContent(openImMsgDTO, companyUser, fsUser.getUserId());
+        if (parseResult.hasError()) {
+            return parseResult.getError();
+        }
+        String content = processContent(parseResult.getContent());
+
+        String conversationId = buildImConversationId(openImMsgDTO);
+        ImChatSession imChatSession = getImChatSession(
+                openImMsgDTO, companyUser, conversationId, companyUser.getUserId(), fsUser.getNickName());
+
+        R aiResponse = dispatchDebouncedAiRequest(
+                openImMsgDTO, content, role, fsUser, conversationId, config, imChatSession, companyUser);
+        clearImReplyCache(conversationId);
+
+        return handleAiReplyResponse(aiResponse, companyUser, role, fsUser, openImMsgDTO, imChatSession);
+    }
+
+    /**
+     * 解析 FsUser 关联的销售:优先 companyUserId,否则经 qwUserId 间接查找
+     */
+    private CompanyUser resolveCompanyUserForFsUser(FsUser fsUser) {
+        CompanyUser companyUser = null;
+        if (fsUser.getCompanyUserId() != null) {
+            companyUser = companyUserMapper.selectCompanyUserByUserId(fsUser.getCompanyUserId());
+        }
+        if (companyUser == null && fsUser.getQwUserId() != null) {
+            QwUser qwUser = qwUserMapper.selectQwUserById(fsUser.getQwUserId());
+            if (qwUser != null && qwUser.getCompanyUserId() != null) {
+                companyUser = companyUserMapper.selectCompanyUserByUserId(qwUser.getCompanyUserId());
+            }
+        }
+        return companyUser;
+    }
+
+    /**
+     * 加载 FastGPT 模式配置;角色或 APPKey 缺失时返回 null(与历史行为一致,非 R.error)
+     */
+    private ModeConfig resolveModeConfig(FastGptRole role) {
+        if (role == null) {
+            return null;
+        }
+        String modeConfigJson = role.getModeConfigJson();
+        if (StringUtils.isEmpty(modeConfigJson)) {
+            return null;
+        }
+        ModeConfig config = JSONUtil.toBean(modeConfigJson, ModeConfig.class);
+        if (StringUtils.isEmpty(config.getAPPKey())) {
+            return null;
+        }
+        return config;
+    }
+
+    private String buildImConversationId(OpenImMsgCallBackVO openImMsgDTO) {
+        return "si_" + openImMsgDTO.getRecvID() + "_" + openImMsgDTO.getSendID();
+    }
+
+    /**
+     * 将 IM 多媒体消息转为 AI 可理解的文本。
+     * 图片/视频走 OCR;语音走腾讯云 ASR;普通文本原样返回。
+     */
+    private ImContentParseResult parseImMultimediaContent(OpenImMsgCallBackVO openImMsgDTO,
+                                                          CompanyUser companyUser, Long senderId) {
+        String content = openImMsgDTO.getContent();
+        int contentType = openImMsgDTO.getContentType();
+
+        if (contentType == IM_CONTENT_IMAGE || contentType == IM_CONTENT_VIDEO) {
+            JSONObject contentObject = new JSONObject(openImMsgDTO.getContent());
+            String url = contentType == IM_CONTENT_IMAGE
+                    ? contentObject.getJSONObject("sourcePicture").getString("url")
+                    : contentObject.getString("videoUrl");
+            String imageParse = aiImgUtil.getIMImageParse(url, companyUser, senderId);
+            if (imageParse == null) {
+                return ImContentParseResult.error(R.error("图片解析失败"));
+            }
+            // 视频帧未识别为表情包时,固定提示文案,避免 AI 误判
+            if (!imageParse.contains("表情包") && contentType == IM_CONTENT_VIDEO) {
+                content = "用户发送图片内容:" + "\"未被识别的表情包\"";
+            } else {
+                String emoticon = imgEmoticon(imageParse);
+                content = (emoticon == null || emoticon.isEmpty())
+                        ? "用户发送图片内容:" + "\"" + imageParse + "\""
+                        : emoticon;
+            }
+        } else if (contentType == IM_CONTENT_VOICE) {
+            JSONObject contentObject = new JSONObject(openImMsgDTO.getContent());
+            String url = contentObject.getString("sourceUrl");
+            if (StringUtils.isEmpty(url)) {
+                return ImContentParseResult.error(R.error("语音解析失败,未获取到语音地址!"));
+            }
+            // 腾讯云 ASR:先上传音频拿 TaskId,再轮询识别结果
+            String uploadStr = TencentAudioUtils.doRequest(url, 0L, 1);
+            if (uploadStr == null) {
+                return ImContentParseResult.error(R.error("语音解析失败!上传录音失败!"));
+            }
+            JSONObject uploadObject = new JSONObject(uploadStr);
+            if (!uploadObject.getJSONObject("Response").isNull("Error")) {
+                log.error("腾讯云语音上传失败!{}", uploadObject.getJSONObject("Response")
+                        .getJSONObject("Error").getString("Message"));
+                return ImContentParseResult.error(R.error("语音解析失败!"));
+            }
+            Long taskId = uploadObject.getJSONObject("Response").getJSONObject("Data").getLong("TaskId");
+            String voiceText = TencentAudioUtils.doRequest("", taskId, 2);
+            if (voiceText == null) {
+                return ImContentParseResult.error(R.error("语音解析失败!获取语音内容失败!"));
+            }
+            content = voiceText;
+        }
+        return ImContentParseResult.success(content);
+    }
+
+    /**
+     * IM 连发防抖:Redis 累加 reply 计数并缓存最新消息。
+     * sendFsUserIMAiMsg 内 sleep 5s 后校验 reply 是否仍为 slot=1,否则丢弃过期请求。
+     */
+    private R dispatchDebouncedAiRequest(OpenImMsgCallBackVO openImMsgDTO, String content,
+                                           FastGptRole role, FsUser fsUser, String conversationId,
+                                           ModeConfig config, ImChatSession imChatSession,
+                                           CompanyUser companyUser) {
+        String replyKey = IM_REPLY_CACHE_KEY + conversationId;
+        String msgKey = IM_MSG_CACHE_KEY + conversationId;
+
+        Integer replyCount = redisCache.getCacheObject(replyKey);
+        if (replyCount == null) {
+            redisCache.setCacheObject(replyKey, 1, IM_REPLY_CACHE_MINUTES, TimeUnit.MINUTES);
+        } else {
+            redisCache.setCacheObject(replyKey, replyCount + 1, IM_REPLY_CACHE_MINUTES, TimeUnit.MINUTES);
+        }
+        redisCache.setCacheObject(msgKey, content, IM_REPLY_CACHE_MINUTES, TimeUnit.MINUTES);
+        return sendFsUserIMAiMsg(IM_REPLY_SLOT, openImMsgDTO, content, role, fsUser,
+                conversationId, config, imChatSession, companyUser);
+    }
+
+    private void clearImReplyCache(String conversationId) {
+        redisCache.deleteObject(IM_REPLY_CACHE_KEY + conversationId);
+        redisCache.deleteObject(IM_MSG_CACHE_KEY + conversationId);
+    }
+
+    /**
+     * 处理 FastGPT 响应:成功则落库并下发 IM 消息,失败则通知销售
+     *
+     * @param aiResponse
+     * @param companyUser
+     * @param role
+     * @param fsUser
+     * @param openImMsgDTO
+     * @param imChatSession
+     * @return
+     */
+    private R handleAiReplyResponse(R aiResponse, CompanyUser companyUser, FastGptRole role,
+                                    FsUser fsUser, OpenImMsgCallBackVO openImMsgDTO,
+                                    ImChatSession imChatSession) {
+        if (aiResponse.get("code").equals(200)) {
+            ChatDetailTStreamFResult result = (ChatDetailTStreamFResult) aiResponse.get("data");
+            if (result.getChoices().isEmpty()) {
+                return R.error("AI回复空消息");
+            }
+            addSaveImChatMsg(companyUser, role, result.getChoices().get(0).getMessage().getContent(), imChatSession);
+            dealWithFsUserResult(result, companyUser, fsUser, openImMsgDTO, fsUser.getUserId(), role, imChatSession);
+            return R.ok();
+        }
+        notifySalesViaIM(companyUser, fsUser, "AI报错", openImMsgDTO);
+        return R.error("AI请求失败");
+    }
+
+    /** IM 多媒体解析结果:error 非空表示需中断主流程 */
+    private static final class ImContentParseResult {
+        private final R error;
+        private final String content;
+
+        private ImContentParseResult(R error, String content) {
+            this.error = error;
+            this.content = content;
+        }
+
+        static ImContentParseResult success(String content) {
+            return new ImContentParseResult(null, content);
+        }
+
+        static ImContentParseResult error(R error) {
+            return new ImContentParseResult(error, null);
+        }
+
+        boolean hasError() {
+            return error != null;
+        }
+
+        R getError() {
+            return error;
+        }
+
+        String getContent() {
+            return content;
+        }
+    }
+
+    /**
+     * 基于 FsUser 调用 FastGPT。
+     * 等待 5s 后校验 reply 槽位:连发场景下仅最新请求继续,较早请求直接返回 error。
+     */
+    private R sendFsUserIMAiMsg(int replySlot, OpenImMsgCallBackVO openImMsgDTO, String content,
+                                FastGptRole role, FsUser fsUser, String conversationId,
+                                ModeConfig config, ImChatSession imChatSession, CompanyUser companyUser) {
+        String replyKey = IM_REPLY_CACHE_KEY + conversationId;
+        try {
+            Thread.sleep(5000);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            log.warn("IM AI 防抖等待被中断, conversationId={}", conversationId);
+        }
+        if (!Objects.equals(redisCache.getCacheObject(replyKey), replySlot)) {
+            return R.error();
+        }
+
+        ChatParam param = new ChatParam();
+        param.setChatId(imChatSession.getChatId());
+        param.setStream(false);
+        param.setDetail(true);
+        ChatParam.Variables variables = new ChatParam.Variables();
+        variables.setUid(openImMsgDTO.getRecvID());
+        variables.setName(fsUser.getNickName());
+        List<ChatParam.Message> messageList = new ArrayList<>();
+        param.setMessages(messageList);
+
+        addFsUserIMKeyWord(role, fsUser, messageList, content, openImMsgDTO, imChatSession);
+
+        R response = chatService.initiatingTakeChat(param, "http://129.28.170.206:3000/api", config.getAPPKey());
+        recordFsUserAiTokenUsage(response, content, fsUser, companyUser);
+
+        if (!Objects.equals(redisCache.getCacheObject(replyKey), replySlot)) {
+            return R.error();
+        }
+        addSaveImChatMsg(companyUser, role, param.getMessages().get(0).getContent(), imChatSession);
+        return response;
+    }
+
+    /** 非知识库命中时记录 FastGPT token 消耗 */
+    private void recordFsUserAiTokenUsage(R response, String content, FsUser fsUser, CompanyUser companyUser) {
+        Object data = response.get("data");
+        if (data instanceof KnowledgeBaseResult) {
+            return;
+        }
+        ChatDetailTStreamFResult chatResult = (ChatDetailTStreamFResult) data;
+        EventLogUtils.recordEventTokenLog(content + "消耗token", fsUser.getUserId(), 2,
+                (long) chatResult.getUsage().getTotal_tokens(), 0, companyUser);
+        EventLogUtils.recordEventTokenLog(content + "输入token", fsUser.getUserId(), 2,
+                (long) chatResult.getUsage().getPrompt_tokens(), 1, companyUser);
+        EventLogUtils.recordEventTokenLog(content + "输出token", fsUser.getUserId(), 2,
+                (long) chatResult.getUsage().getCompletion_tokens(), 2, companyUser);
+    }
+
+    /**
+     * 基于 FsUser 构建 AI 上下文(FastGptChatConversation JSON)
+     * userInfo 字段配置见 {@link FastGptRole#getUserInfo()},contactInfo 仍用于企微渠道
+     */
+    private void addFsUserIMKeyWord(FastGptRole role, FsUser fsUser, List<ChatParam.Message> messageList,
+                                    String content, OpenImMsgCallBackVO openImMsgDTO,
+                                    ImChatSession imChatSession) {
+
+        FastGptChatConversation conversation = new FastGptChatConversation();
+        conversation.setUserInfo(new com.alibaba.fastjson.JSONObject());
+        conversation.setHistory(new com.alibaba.fastjson.JSONArray());
+
+        if (role.getReminderWords() != null && !role.getReminderWords().isEmpty()) {
+            conversation.setAiInfo(role.getReminderWords());
+        }
+
+        fillFsUserInfo(conversation, role, fsUser, imChatSession);
+
+        com.alibaba.fastjson.JSONArray historyArray = buildFsUserImHistory(imChatSession, openImMsgDTO);
+        if (!historyArray.isEmpty()) {
+            conversation.setHistory(historyArray);
+        }
+
+        if (content != null && !content.isEmpty()) {
+            conversation.setUserContent(content);
+        }
+
+        ChatParam.Message message1 = new ChatParam.Message();
+        message1.setRole("user");
+        message1.setContent(new Gson().toJson(conversation));
+        messageList.add(message1);
+    }
+
+    /**
+     * 按角色 userInfo 配置(逗号分隔 Java 字段名),从 ImChatSession、FsUser 填充对话 userInfo
+     */
+    private void fillFsUserInfo(FastGptChatConversation conversation, FastGptRole role,
+                                FsUser fsUser, ImChatSession imChatSession) {
+        String sessionUserInfo = imChatSession.getUserInfo();
+        String[] split = role.getUserInfo().split(",");
+        com.alibaba.fastjson.JSONObject userInfo = conversation.getUserInfo();
+        if(sessionUserInfo != null){
+            putBeanFields(userInfo, split, fsUser);
+        }else{
+            for (String name : split) {
+                if (name != null) {
+                    userInfo.put(name,"");
+                }
+            }
+        }
+    }
+
+
+    private void putBeanFields(com.alibaba.fastjson.JSONObject userInfo, String[] fieldNames, Object bean) {
+        if (bean == null || fieldNames == null) {
+            return;
+        }
+        Class<?> clazz = bean.getClass();
+        for (String rawName : fieldNames) {
+            if (rawName == null) {
+                continue;
+            }
+            String name = rawName.trim();
+            if (name.isEmpty()) {
+                continue;
+            }
+            try {
+                Field field = clazz.getDeclaredField(name);
+                field.setAccessible(true);
+                Object value = field.get(bean);
+                String jsonKey = resolveFieldJsonKey(field, name);
+                if (value != null) {
+                    userInfo.put(jsonKey, value.toString());
+                } else if (!userInfo.containsKey(jsonKey)) {
+                    userInfo.put(jsonKey, "");
+                }
+            } catch (NoSuchFieldException | IllegalAccessException ignored) {
+                // 当前 bean 无此字段,由其他数据源补充
+            }
+        }
+    }
+
+    /** 有 @Excel 注解时用中文名作为 JSON key,与 AI 回复中的【用户状态信息】块一致 */
+    private String resolveFieldJsonKey(Field field, String fieldName) {
+        Excel excel = field.getAnnotation(Excel.class);
+        if (excel != null && StringUtils.isNotEmpty(excel.name())) {
+            return excel.name();
+        }
+        return fieldName;
+    }
+
+    /**
+     * 构建 IM 渠道历史对话(JSON 数组,role: user/ai)
+     */
+    private com.alibaba.fastjson.JSONArray buildFsUserImHistory(ImChatSession imChatSession,
+                                                                OpenImMsgCallBackVO openImMsgDTO) {
+        com.alibaba.fastjson.JSONArray historyArray = new com.alibaba.fastjson.JSONArray();
+
+        if (imChatSession != null && imChatSession.getSessionId() != null) {
+            List<ImChatMsg> msgs = imChatMsgService.getListBySessionId(imChatSession.getSessionId());
+            if (msgs != null && !msgs.isEmpty()) {
+                String sessionUserId = imChatSession.getUserId();
+                for (ImChatMsg msg : msgs) {
+                    String msgContent = StringUtils.isNotEmpty(msg.getShortContent())
+                            ? msg.getShortContent() : msg.getContent();
+                    boolean isUser;
+                    if (msg.getUserType() != null) {
+                        isUser = msg.getUserType() == 1;
+                    } else {
+                        isUser = sessionUserId != null && sessionUserId.equals(msg.getUserId());
+                    }
+                    if (!isUser && msgContent != null && msgContent.length() > 500) {
+                        continue;
+                    }
+                    com.alibaba.fastjson.JSONObject msgObj = new com.alibaba.fastjson.JSONObject();
+                    msgObj.put("role", isUser ? "user" : "ai");
+                    msgObj.put("content", msgContent);
+                    historyArray.add(msgObj);
+                }
+                return historyArray;
+            }
+        }
+
+        if (openImMsgDTO == null) {
+            return historyArray;
+        }
+        List<FsInquiryOrderMsg> historyMsg = fsInquiryOrderMsgService.selectHistoryMsgByUserId(
+                stripPrefix(openImMsgDTO.getSendID(), "U"),
+                stripPrefix(openImMsgDTO.getRecvID(), "C"));
+        if (historyMsg == null || historyMsg.isEmpty()) {
+            return historyArray;
+        }
+        Collections.reverse(historyMsg);
+        for (FsInquiryOrderMsg inquiryMsg : historyMsg) {
+            String hisContent = inquiryMsg.getContent();
+            int contentType = inquiryMsg.getMsgContentType() != null ? inquiryMsg.getMsgContentType() : 1;
+            if (contentType != 1 && hisContent != null && hisContent.length() > 500) {
+                continue;
+            }
+            com.alibaba.fastjson.JSONObject msgObj = new com.alibaba.fastjson.JSONObject();
+            msgObj.put("role", "1".equals(inquiryMsg.getMsgType()) ? "user" : "ai");
+            msgObj.put("content", hisContent);
+            historyArray.add(msgObj);
+        }
+        return historyArray;
+    }
+
+    /**
+     * 解析 AI 回复中的【用户状态信息】,回写 ImChatSession / FsUser(字段范围由 role.userInfo 配置)
+     */
+    private void updateImSessionUserInfoFromAi(String word, FastGptRole role,
+                                                ImChatSession imChatSession, FsUser fsUser) {
+        if (role == null || StringUtils.isEmpty(role.getUserInfo()) || StringUtils.isEmpty(word)) {
+            return;
+        }
+        Pattern pattern = Pattern.compile("【用户状态信息(.*?)】", Pattern.DOTALL);
+        Matcher matcher = pattern.matcher(word);
+        if (!matcher.find()) {
+            return;
+        }
+        String[] lines = matcher.group(1).trim().split("\n");
+        String[] fieldNames = role.getUserInfo().split(",");
+        boolean sessionUpdated = false;
+        boolean userUpdated = false;
+
+        if (imChatSession != null) {
+            sessionUpdated = applyAiUserInfoLines(lines, fieldNames, imChatSession);
+        }
+        if (fsUser != null) {
+            userUpdated = applyAiUserInfoLines(lines, fieldNames, fsUser);
+        }
+
+        if (sessionUpdated && imChatSession.getSessionId() != null) {
+            ImChatSession update = new ImChatSession();
+            update.setSessionId(imChatSession.getSessionId());
+            update.setStudy(imChatSession.getStudy());
+            update.setCourseStatus(imChatSession.getCourseStatus());
+            update.setCourseName(imChatSession.getCourseName());
+            update.setVideoName(imChatSession.getVideoName());
+            update.setTalk(imChatSession.getTalk());
+            update.setLastTalkTime(new Date());
+            imChatSessionService.updateImChatSession(update);
+        }
+        if (userUpdated && fsUser.getUserId() != null) {
+            fsUserMapper.updateFsUser(fsUser);
+        }
+    }
+
+    private boolean applyAiUserInfoLines(String[] lines, String[] fieldNames, Object bean) {
+        boolean updated = false;
+        Class<?> clazz = bean.getClass();
+        for (String rawName : fieldNames) {
+            if (rawName == null) {
+                continue;
+            }
+            String fieldName = rawName.trim();
+            if (fieldName.isEmpty()) {
+                continue;
+            }
+            try {
+                Field field = clazz.getDeclaredField(fieldName);
+                field.setAccessible(true);
+                String jsonKey = resolveFieldJsonKey(field, fieldName);
+                for (String line : lines) {
+                    String[] kv = splitUserInfoLine(line);
+                    if (kv == null || kv.length != 2) {
+                        continue;
+                    }
+                    if (!kv[0].trim().equals(jsonKey)) {
+                        continue;
+                    }
+                    String newValue = kv[1].trim();
+                    if (newValue.isEmpty()) {
+                        continue;
+                    }
+                    if ("学习到的章节".equals(jsonKey) || "今日课程完成情况".equals(jsonKey)) {
+                        continue;
+                    }
+                    if (newValue.contains("delete")) {
+                        if (newValue.contains(";")) {
+                            newValue = newValue.replaceAll("delete.*?;", "");
+                        } else {
+                            newValue = " ";
+                        }
+                    }
+                    Object oldValue = field.get(bean);
+                    String oldStr = oldValue != null ? oldValue.toString().trim() : "";
+                    if (!oldStr.equals(newValue)) {
+                        field.set(bean, newValue);
+                        updated = true;
+                    }
+                    break;
+                }
+            } catch (NoSuchFieldException | IllegalAccessException ignored) {
+                // 当前 bean 无此字段
+            }
+        }
+        return updated;
+    }
+
+    private String[] splitUserInfoLine(String line) {
+        if (line == null) {
+            return null;
+        }
+        if (line.contains(":")) {
+            return line.split(":", 2);
+        }
+        if (line.contains(":")) {
+            return line.split(":", 2);
+        }
+        return null;
+    }
+
+    /**
+     * 处理AI回复结果(基于FsUser版本)
+     */
+    private void dealWithFsUserResult(ChatDetailTStreamFResult result, CompanyUser companyUser,
+                                       FsUser fsUser, OpenImMsgCallBackVO openImMsgDTO, Long sender,
+                                       FastGptRole role, ImChatSession imChatSession) {
+        String contentKh = result.getChoices().get(0).getMessage().getContent();
+        String conversationId = "si_" + openImMsgDTO.getRecvID() + "_" + openImMsgDTO.getSendID();
+        Gson gson = new Gson();
+        FastGptChatConversation fastGptChatConversation = gson.fromJson(contentKh, FastGptChatConversation.class);
+        String content = replace(fastGptChatConversation.getAiContent()).trim();
+
+        if (!content.isEmpty()) {
+            // 转人工关键词判断
+            List<String> collect = qwExternalContactMapper.selectChatGptChatArtificialWords().stream().map(m -> m.getContent()).collect(Collectors.toList());
+            if (collect.stream().anyMatch(contentKh::contains)) {
+                log.info(conversationId + " :触发转人工:" + contentKh);
+                markSessionAsHuman(imChatSession, openImMsgDTO);
+                // 通过IM通知销售
+                notifySalesViaIM(companyUser, fsUser, " 触发关键词", openImMsgDTO);
+                return;
+            }
+            if (result.isLongText()) {
+                sendIMMsg(content, companyUser, openImMsgDTO);
+            } else {
+                String sa = contentKh.replaceAll("】\n", "】").replaceAll("\n【", "【");
+                String nr = replace(sa);
+                String[] split = nr.split("\n");
+                if (split.length > 6) {
+                    log.info(conversationId + " :消息过长,转人工");
+                    markSessionAsHuman(imChatSession, openImMsgDTO);
+                    notifySalesViaIM(companyUser, fsUser, " 回复长度异常", openImMsgDTO);
+                    return;
+                }
+                List<String> countList = countString(content);
+                updateImSessionUserInfoFromAi(contentKh, role, imChatSession, fsUser);
+                for (String msg : countList) {
+                    sendIMMsg(msg, companyUser, openImMsgDTO);
+                    try {
+                        Thread.sleep(10000);
+                    } catch (InterruptedException ignored) {
+                    }
+                }
+            }
+        }
+        // AI主动请求转人工
+        if (result.isArtificial()) {
+            log.info(conversationId + " :AI请求转人工:" + contentKh);
+            markSessionAsHuman(imChatSession, openImMsgDTO);
+            notifySalesViaIM(companyUser, fsUser, " AI请求人工协助", openImMsgDTO);
+        }
+    }
+
+    /**
+     * 标记会话为人工模式
+     */
+    private void markSessionAsHuman(ImChatSession imChatSession, OpenImMsgCallBackVO openImMsgDTO) {
+        ImChatSession updateSession = new ImChatSession();
+        updateSession.setSessionId(imChatSession.getSessionId());
+        String humanKey = "IM:SESSION:HUMAN:" + imChatSession.getChatId();
+        redisCache.setCacheObject(humanKey, "1", 10, TimeUnit.MINUTES);
+        updateSession.setPersonOrAi(1);
+        imChatSessionService.updateImChatSession(updateSession);
+        OpenIMUtil.editConversationEx(openImMsgDTO, imConfig, redisCache, 1);
+    }
+
+    /**
+     * 通过IM通知销售转人工
+     */
+    private void notifySalesViaIM(CompanyUser companyUser, FsUser fsUser, String reason, OpenImMsgCallBackVO openImMsgDTO) {
+        log.info("IM转人工通知: fsUser={}, reason={}", fsUser.getNickName(), reason);
+        try {
+            // 向用户发送转人工提示
+            sendIMMsg("正在为您转接人工客服,请稍等...", companyUser, openImMsgDTO);
+
+            // 通过IM给销售发送转人工通知消息
+            OpenImMsgDTO notifyMsg = new OpenImMsgDTO();
+            OpenImMsgDTO.OfflinePushInfo offlinePushInfo = new OpenImMsgDTO.OfflinePushInfo();
+            offlinePushInfo.setTitle("转人工通知");
+            offlinePushInfo.setDesc("客户" + fsUser.getNickName() + "需要人工服务");
+            offlinePushInfo.setIOSBadgeCount(true);
+
+            // 销售发送消息给用户,这里反过来:系统给销售发通知
+            // 发送者用系统ID,接收者用销售的IM ID(C + companyUserId)
+            String salesImId = "C" + companyUser.getUserId();
+            notifyMsg.setSendID(openImMsgDTO.getRecvID()); // 用原来的接收者(客服/销售ID)作为发送者
+            notifyMsg.setRecvID(salesImId); // 接收者是销售
+            notifyMsg.setContentType(101);
+            notifyMsg.setSenderPlatformID(openImMsgDTO.getSenderPlatformID());
+            notifyMsg.setSessionType(1); // 单聊
+
+            ObjectMapper objectMapper = new ObjectMapper();
+            OpenImMsgDTO.Content msgContent = new OpenImMsgDTO.Content();
+            PayloadDTO payload = new PayloadDTO();
+            String notifyText = "【转人工通知】客户 " + fsUser.getNickName() + " 因" + reason + " 需要人工服务,请及时回复";
+            payload.setData(notifyText);
+            PayloadDTO.Extension extension = new PayloadDTO.Extension();
+            extension.setTitle("转人工通知");
+            payload.setExtension(extension);
+            OpenImMsgDTO.ImData imData = new OpenImMsgDTO.ImData();
+            imData.setPayload(payload);
+            String imJson = objectMapper.writeValueAsString(imData);
+            msgContent.setData(imJson);
+            msgContent.setContent(notifyText);
+            notifyMsg.setContent(msgContent);
+            notifyMsg.setOfflinePushInfo(offlinePushInfo);
+
+            OpenIMUtil.openIMSendMsg(notifyMsg, imConfig, redisCache);
+        } catch (Exception e) {
+            log.error("通过IM通知销售转人工失败: {}", e.getMessage(), e);
+        }
+    }
+
+    private void sendIMMsg(String msg, CompanyUser companyUser, OpenImMsgCallBackVO openImMsgDTO) {
+        try {
+            if (msg == null || msg.trim().isEmpty()) {
+                log.info("输出为空格");
+                return;
+            }
+            // 违规词语替换
+            msg = replaceWords(msg);
+            OpenImMsgDTO.OfflinePushInfo offlinePushInfo = new OpenImMsgDTO.OfflinePushInfo();
+            OpenImMsgDTO replyMsg = new OpenImMsgDTO();
+            if (null != companyUser && StringUtils.isNotEmpty(companyUser.getAvatar())) {
+                offlinePushInfo.setTitle(companyUser.getNickName());
+                replyMsg.setSenderFaceURL(companyUser.getAvatar());
+            }
+            // 初始化ObjectMapper对象,用于JSON序列化和反序列化
+            ObjectMapper objectMapper = new ObjectMapper();
+            // 设置消息的发送者ID、接收者ID、内容类型等基础信息
+            // 将发送者和接收者交换一下
+            replyMsg.setSendID(openImMsgDTO.getRecvID());
+            replyMsg.setRecvID(openImMsgDTO.getSendID());
+            replyMsg.setContentType(101);
+            replyMsg.setSenderPlatformID(openImMsgDTO.getSenderPlatformID());
+            replyMsg.setSessionType(openImMsgDTO.getSessionType());
+            // 初始化消息内容对象
+            OpenImMsgDTO.Content content = new OpenImMsgDTO.Content();
+            // 初始化负载数据对象,并设置数据内容
+            PayloadDTO payload = new PayloadDTO();
+            payload.setData(msg);
+            PayloadDTO.Extension extension = new PayloadDTO.Extension();
+            extension.setTitle("快速回复"); // 可选标题
+            payload.setExtension(extension);
+            OpenImMsgDTO.ImData imData = new OpenImMsgDTO.ImData();
+            imData.setPayload(payload);
+            String imJson = objectMapper.writeValueAsString(imData);
+            content.setData(imJson);
+            content.setContent(msg);
+            replyMsg.setContent(content);
+            offlinePushInfo.setDesc("快速回复");
+            offlinePushInfo.setIOSBadgeCount(true);
+            offlinePushInfo.setIOSPushSound("");
+            replyMsg.setOfflinePushInfo(offlinePushInfo);
+            // 发送消息
+            OpenImResponseDTO response = OpenIMUtil.openIMSendMsg(replyMsg, imConfig, redisCache);
+        } catch (Exception e) {
+            log.error("修改会话异常:{}", e);
+        }
+    }
+
+
+    private void addIMKeyWord(String countInfo, Long extId, List<ChatParam.Message> messageList, String words, String content, OpenImMsgCallBackVO openImMsgDTO) {
+        String str = "";
+        // 这里获取后台的提示词进行匹配
+        QwExternalContactInfo info = qwExternalContactInfoMapper.selectQwExternalContactInfoByExternalContactId(extId);
+        if (info == null) {
+            info = new QwExternalContactInfo();
+        }
+        if (info != null) {
+            str = "【用户状态信息\n";
+            Field[] fields = info.getClass().getDeclaredFields();
+            for (Field field : fields) {
+                field.setAccessible(true);
+                Excel annotation = field.getAnnotation(Excel.class);
+                if (annotation != null) {
+                    String name = field.getName();
+                    String fieldName = annotation.name();
+                    String[] split = countInfo.split(",");
+                    for (String zName : split) {
+                        if (zName.equals(name)) {
+                            Object value = null;
+                            try {
+                                value = field.get(info);
+                            } catch (IllegalAccessException e) {
+                            }
+                            if (value != null) {
+                                str += fieldName + ": " + value.toString() + "\n";
+                            } else {
+                                str += fieldName + ":  \n";
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        str += "】\n";
+
+        if (words != null && !"".equals(words)) {
+            str += "【你的角色信息:以下内容为你的信息状态的补充而非用户信息,相当于放在角色任务里面,问到了需要知晓,但是如果无关的时候请无视此段内容 " + "\"" + words + "\"" + "】\n";
+        }
+        // 无法查询IM历史消息智能查询本地消息
+        List<FsInquiryOrderMsg> historyMsg = fsInquiryOrderMsgService.selectHistoryMsgByUserId(openImMsgDTO.getSendID().replace("U", ""), openImMsgDTO.getRecvID().replace("C", ""));
+        //如果之前有消息 加下面这句话,没消息 跳过
+        if (!historyMsg.isEmpty()) {
+            Collections.reverse(historyMsg);
+            str += "【历史聊天内容:\n";
+            for (FsInquiryOrderMsg fsInquiryOrderMsg : historyMsg) {
+                int contentType = fsInquiryOrderMsg.getMsgContentType();
+                String hisContent = fsInquiryOrderMsg.getContent();
+                if (contentType != 1) {
+                    if (hisContent != null && hisContent.length() > 150) {
+                        continue;
+                    }
+                }
+                str += ("1".equals(fsInquiryOrderMsg.getMsgType()) ? "用户:" : "AI:") + hisContent + "\n";
+            }
+            str += "】\n";
+        }
+        if (content != null && !"".equals(content)) {
+            str += "【用户说的话内容(之前的内容仅仅为背景,你知道即可,以下才是用户真实说的话的内容)\n" +
+                    content + "\n" +
+                    "】";
+        }
+        ChatParam.Message message1 = new ChatParam.Message();
+        message1.setRole("user");
+        message1.setContent(str);
+        messageList.add(message1);
+    }
+
+    private void addSaveImChatMsg(CompanyUser companyUser, FastGptRole role, String content, ImChatSession imChatSession) {
+        ImChatMsg msg = new ImChatMsg();
+        msg.setSessionId(imChatSession.getSessionId());
+        msg.setUserId(String.valueOf(companyUser.getUserId()));
+        msg.setContent(content);
+        msg.setMsgType(2);
+        msg.setCompanyId(companyUser.getCompanyId());
+        msg.setRoleId(role.getRoleId());
+        msg.setCompanyUserId(companyUser.getUserId());
+        msg.setStatus(0);
+        msg.setNickName(companyUser.getNickName());
+        msg.setAvatar(companyUser.getAvatar());
+        msg.setCreateTime(new Date());
+        imChatMsgService.insertImChatMsg(msg);
+    }
+
+    private ImChatSession getImChatSession(OpenImMsgCallBackVO openImMsgDTO, CompanyUser user, String conversationId, Long qwUserId, String name) {
+        ImChatSession session = imChatSessionService.selectImChatSessionByChatId(conversationId);
+        if (session == null) {
+            ImChatSession insert = new ImChatSession();
+            insert.setChatId(conversationId);
+            insert.setUserId(stripPrefix(openImMsgDTO.getSendID(), "U"));
+            insert.setKfId(stripPrefix(openImMsgDTO.getRecvID(), "C"));
+            insert.setCompanyId(user.getCompanyId());
+            insert.setNickName(user.getNickName());
+            insert.setAvatar(user.getAvatar());
+            insert.setCreateTime(new Date());
+            insert.setQwUserId(qwUserId);
+            insert.setUserName(name);
+            insert.setPersonOrAi(2);
+
+            imChatSessionService.insertImChatSession(insert);
+            return insert;
+        }
+
+        boolean needUpdate = false;
+
+        if (session.getQwUserId() == null || !Objects.equals(session.getQwUserId(), qwUserId)) {
+            session.setQwUserId(qwUserId);
+            needUpdate = true;
+        }
+
+        if (!Objects.equals(session.getUserName(), name)) {
+            session.setUserName(name);
+            needUpdate = true;
+        }
+
+        if (StringUtils.isEmpty(session.getNickName())) {
+            session.setNickName(user.getNickName());
+            needUpdate = true;
+        }
+
+        if (StringUtils.isEmpty(session.getUserName())) {
+            session.setUserName(name);
+            needUpdate = true;
+        }
+        if (session.getCompanyId() == null) {
+            session.setCompanyId(user.getCompanyId());
+            needUpdate = true;
+        }
+        if (StringUtils.isEmpty(session.getAvatar())) {
+            session.setAvatar(user.getAvatar());
+            needUpdate = true;
+        }
+
+        if (needUpdate) {
+            imChatSessionService.updateImChatSession(session);
+        }
+
+        return session;
+    }
+
+    private String stripPrefix(String value, String prefix) {
+        if (StringUtils.isEmpty(value)) {
+            return value;
+        }
+        return value.startsWith(prefix) ? value.substring(1) : value;
+    }
+    /*@Override
     public R AiReply(OpenImMsgCallBackVO openImMsgDTO, Long companyId) {
         String content = openImMsgDTO.getContent();
         if (content == null || content.isEmpty() || content.trim().isEmpty()) {
@@ -366,7 +1246,7 @@ public class AiHookServiceImpl implements AiHookService {
         }
         return R.error();
     }
-
+*/
 
     /** Ai回复 **/
     @Async

+ 5 - 0
fs-service/src/main/java/com/fs/fastGpt/service/impl/FastGptRoleServiceImpl.java

@@ -13,6 +13,7 @@ import com.fs.common.utils.DateUtils;
 import com.fs.course.domain.FsUserCourseVideo;
 import com.fs.fastGpt.domain.FastGptRoleTag;
 import com.fs.fastGpt.mapper.FastGptRoleTagMapper;
+import com.fs.fastGpt.param.FastGptRoleNameAndId;
 import com.fs.fastGpt.vo.FastGptRoleDataVO;
 import com.fs.fastGpt.vo.FastGptRoleVO;
 import com.fs.fastGpt.vo.FastgptEventLogTotalVo;
@@ -260,5 +261,9 @@ public class FastGptRoleServiceImpl implements IFastGptRoleService
         return fastGptRoleMapper.selectAllCourseList(companyId);
     }
 
+    @Override
+    public List<FastGptRoleNameAndId> selectFastGptNameAndId(Long companyId) {
+        return fastGptRoleMapper.selectFastGptNameAndId(companyId);
+    }
 
 }

+ 226 - 0
fs-service/src/main/java/com/fs/fastGpt/utils/TencentAudioUtils.java

@@ -0,0 +1,226 @@
+package com.fs.fastGpt.utils;
+
+import okhttp3.*;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+@Component
+public class TencentAudioUtils {
+
+    //读取配置文件里面的账号密码
+    @Value("${tencent.secretId}")
+    private String secretId;
+    @Value("${tencent.secretKey}")
+    private String secretKey;
+    private static String token = "";
+    private static String service = "asr";
+    private static String version = "2019-06-14";
+    private static final Logger log = LoggerFactory.getLogger(TencentAudioUtils.class);
+
+    // 添加静态变量保存实例引用
+    private static TencentAudioUtils instance;
+
+    // 在实例初始化时保存实例引用
+    public void setSecretId(String secretId) {
+        this.secretId = secretId;
+    }
+
+    public void setSecretKey(String secretKey) {
+        this.secretKey = secretKey;
+    }
+
+    // 实例初始化时保存引用
+    public TencentAudioUtils() {
+        instance = this;
+    }
+
+    public static String doRequest(String url, Long taskId,Integer type) {
+        String action;
+        String body;
+        // 1.进行推送
+        if (1 == type) {
+            action = "CreateRecTask";
+            body = "\n" +
+                    "{\n" +
+                    "    \"Url\": \"" +  url + "\",\n" +
+                    "    \"ChannelNum\": 1,\n" +
+                    "    \"EngineModelType\": \"16k_zh\",\n" +
+                    "    \"ResTextFormat\": 0,\n" +
+                    "    \"SourceType\": 0\n" +
+                    "}";
+        } else if (2 == type) {
+            action = "DescribeTaskStatus";
+            body = "\n" +
+                    "{\n" +
+                    "    \"TaskId\": " + taskId +
+                    "}";
+        } else {
+            return null;
+        }
+        try {
+            if (2 == type) {
+                String result = null;
+                while (true) {
+                    String checkResult = doRequest(instance.secretId, instance.secretKey, service, version, action, body, "ap-guangzhou", token);
+                    JSONObject jsonObject = new JSONObject(checkResult);
+                    JSONObject data = jsonObject.getJSONObject("Response").getJSONObject("Data");
+                    String statusStr = data.getString("StatusStr");
+                    if ("failed".equals(statusStr)) {
+                        break;
+                    }else if ("success".equals(statusStr)) {
+                        result = extractText(data.getString("Result"));
+                        break;
+                    }
+                    Thread.sleep(2000);
+                }
+                return result;
+            }
+            return doRequest(instance.secretId, instance.secretKey, service, version, action, body, "ap-guangzhou", token);
+        } catch (Exception e) {
+            log.error("腾讯云请求异常");
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 从带时间戳的文本中提取纯文字内容
+     * @param input 带时间戳的原始文本
+     * @return 提取后的纯文字内容
+     */
+    public static String extractText(String input) {
+        // 使用正则表达式匹配时间戳部分并替换为空
+        // 匹配模式: [数字:小数,数字:小数]
+        String regex = "\\[\\d+:\\d+\\.\\d+,\\d+:\\d+\\.\\d+\\]\\s*";
+        return input.replaceAll(regex, "").trim();
+    }
+
+    // singleton client for connection reuse and better performance
+    private static final OkHttpClient client = new OkHttpClient();
+
+    public static String doRequest(
+            String secretId, String secretKey,
+            String service, String version, String action,
+            String body, String region, String token
+    ) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
+
+        Request request = buildRequest(secretId, secretKey, service, version, action, body, region, token);
+
+        Response response = client.newCall(request).execute();
+        return response.body().string();
+    }
+
+    public static Request buildRequest(
+            String secretId, String secretKey,
+            String service, String version, String action,
+            String body, String region, String token
+    ) throws NoSuchAlgorithmException, InvalidKeyException {
+        String host = "asr.tencentcloudapi.com";
+        String endpoint = "https://" + host;
+        String contentType = "application/json; charset=utf-8";
+        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+        String auth = getAuth(secretId, secretKey, host, contentType, timestamp, body);
+        return new Request.Builder()
+                .header("Host", host)
+                .header("X-TC-Timestamp", timestamp)
+                .header("X-TC-Version", version)
+                .header("X-TC-Action", action)
+                .header("X-TC-Region", region)
+                .header("X-TC-Token", token)
+                .header("X-TC-RequestClient", "SDK_JAVA_BAREBONE")
+                .header("Authorization", auth)
+                .url(endpoint)
+                .post(RequestBody.create(MediaType.parse(contentType), body))
+                .build();
+    }
+
+    private static String getAuth(
+            String secretId, String secretKey, String host, String contentType,
+            String timestamp, String body
+    ) throws NoSuchAlgorithmException, InvalidKeyException {
+        String canonicalUri = "/";
+        String canonicalQueryString = "";
+        String canonicalHeaders = "content-type:" + contentType + "\nhost:" + host + "\n";
+        String signedHeaders = "content-type;host";
+
+        String hashedRequestPayload = sha256Hex(body.getBytes(StandardCharsets.UTF_8));
+        String canonicalRequest = "POST"
+                + "\n"
+                + canonicalUri
+                + "\n"
+                + canonicalQueryString
+                + "\n"
+                + canonicalHeaders
+                + "\n"
+                + signedHeaders
+                + "\n"
+                + hashedRequestPayload;
+
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
+        String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
+        String service = host.split("\\.")[0];
+        String credentialScope = date + "/" + service + "/" + "tc3_request";
+        String hashedCanonicalRequest =
+                sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8));
+        String stringToSign =
+                "TC3-HMAC-SHA256\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest;
+
+        byte[] secretDate = hmac256(("TC3" + secretKey).getBytes(StandardCharsets.UTF_8), date);
+        byte[] secretService = hmac256(secretDate, service);
+        byte[] secretSigning = hmac256(secretService, "tc3_request");
+        String signature =
+                printHexBinary(hmac256(secretSigning, stringToSign)).toLowerCase();
+        return "TC3-HMAC-SHA256 "
+                + "Credential="
+                + secretId
+                + "/"
+                + credentialScope
+                + ", "
+                + "SignedHeaders="
+                + signedHeaders
+                + ", "
+                + "Signature="
+                + signature;
+    }
+
+    public static String sha256Hex(byte[] b) throws NoSuchAlgorithmException {
+        MessageDigest md;
+        md = MessageDigest.getInstance("SHA-256");
+        byte[] d = md.digest(b);
+        return printHexBinary(d).toLowerCase();
+    }
+
+    private static final char[] hexCode = "0123456789ABCDEF".toCharArray();
+
+    public static String printHexBinary(byte[] data) {
+        StringBuilder r = new StringBuilder(data.length * 2);
+        for (byte b : data) {
+            r.append(hexCode[(b >> 4) & 0xF]);
+            r.append(hexCode[(b & 0xF)]);
+        }
+        return r.toString();
+    }
+
+    public static byte[] hmac256(byte[] key, String msg) throws NoSuchAlgorithmException, InvalidKeyException {
+        Mac mac = Mac.getInstance("HmacSHA256");
+        SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
+        mac.init(secretKeySpec);
+        return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8));
+    }
+
+}

+ 4 - 0
fs-service/src/main/java/com/fs/fastGpt/vo/FastGptRoleVO.java

@@ -57,5 +57,9 @@ public class FastGptRoleVO  extends BaseEntity  {
      * 绑定的公司
      */
     private String bindCorpId;
+
+    /** 需要获取的客户信息(IM 渠道,逗号分隔字段名) */
+    private String userInfo;
+
     private Integer isTags;
 }

+ 64 - 0
fs-service/src/main/java/com/fs/fastgptApi/util/AiImgUtil.java

@@ -1,6 +1,7 @@
 package com.fs.fastgptApi.util;
 
 import com.alibaba.fastjson.JSON;
+import com.fs.company.domain.CompanyUser;
 import com.fs.fastgptApi.param.DouBaoAiParam;
 import com.fs.fastgptApi.result.AiImgResult;
 import com.fs.qw.domain.QwUser;
@@ -299,4 +300,67 @@ public class AiImgUtil {
             }
         }
     }*/
+
+    public String getIMImageParse(String imageUrl, CompanyUser companyUser, Long sender) {
+        try {
+            DouBaoAiParam.Content imageContent = new DouBaoAiParam.Content();
+            imageContent.setType("image_url");
+            imageContent.setImage_url(new DouBaoAiParam.imageUrl());
+            imageContent.getImage_url().setUrl(imageUrl);
+
+
+            DouBaoAiParam.Content textContent = new DouBaoAiParam.Content();
+            textContent.setType("text");
+            textContent.setText(
+                    "识别图片内容 \n" +
+                            "情况一:图片为表情包的时候或是明确意义图片的时候,单独提取出表情包的含义为图片,并输出:【表情包:XXX】XXX为表情表达的内容,例如这个表情包是很开心的感谢,那么XXX就是谢谢。在【】外不进行其他的解释直接结束 \n" +
+                            "情况二:图片是舌头的时候,根据他的舌苔进行简单的分析,直接输出 \n" +
+                            "情况三:图片是其他的时候,正常提取图片内容,如果是身体异常部位要进行简单分析,直接输出,如果是卡通图片,需要在结尾输出【这是卡通图片】这几个字");
+
+
+            List<DouBaoAiParam.Content> contents = new ArrayList<>();
+            contents.add(imageContent);
+            contents.add(textContent);
+
+
+            DouBaoAiParam.Message message = new DouBaoAiParam.Message();
+            message.setRole("user");
+            message.setContent(contents);
+
+            // Add message to list
+            List<DouBaoAiParam.Message> messages = new ArrayList<>();
+
+            DouBaoAiParam.Content textContent2 = new DouBaoAiParam.Content();
+            textContent2.setType("text");
+            textContent2.setText(
+                    "识别图片内容 情况一:图片为表情包的时候或是明确意义图片的时候,单独提取出表情包的含义为图片,并输出:【表情包:XXX】XXX为表情表达的内容,例如这个表情包是很开心的感谢,那么XXX就是谢谢。在【】外不进行其他的解释直接结束 情况二:图片是舌头的时候,根据他的舌苔进行简单的分析,直接输出 情况三:图片是其他的时候,正常提取图片内容,如果是身体异常部位要进行简单分析,直接输出");
+
+
+            List<DouBaoAiParam.Content> contents2 = new ArrayList<>();
+            contents2.add(textContent2);
+            DouBaoAiParam.Message message2 = new DouBaoAiParam.Message();
+            message2.setRole("system");
+            message2.setContent(contents2);
+            // messages.add(message2);
+            messages.add(message);
+            // Create request
+            DouBaoAiParam request = new DouBaoAiParam();
+            request.setModel("doubao-1-5-thinking-vision-pro-250428");
+            request.setMessages(messages);
+
+            String jsonString = JSON.toJSONString(request);
+            log.info(jsonString);
+            // 发送请求
+            String response = sendAiImgHttpRequest(jsonString);
+            log.info("API响应: " + response);
+            AiImgResult aiImgResult = JSON.parseObject(response, AiImgResult.class);
+            EventLogUtils.createEventTokenLog("读取图片",companyUser,sender,aiImgResult);
+            EventLogUtils.recordEventLog(sender,1L,8, companyUser);
+            String content = aiImgResult.getChoices().get(0).getMessage().getContent();
+            return content;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
 }

+ 65 - 0
fs-service/src/main/java/com/fs/fastgptApi/util/EventLogUtils.java

@@ -1,6 +1,7 @@
 package com.fs.fastgptApi.util;
 
 import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.company.domain.CompanyUser;
 import com.fs.fastGpt.domain.FastGptEventLog;
 import com.fs.fastGpt.domain.FastGptEventTokenLog;
 import com.fs.fastGpt.service.IFastGptChatMsgService;
@@ -107,6 +108,29 @@ public class EventLogUtils {
         EventLogQueue.addEventTokenLog(fastGptEventTokenLog); // 入队
         //fastGptChatMsgService.insertFastGptEventTokenLog(fastGptEventTokenLog);
     }
+    /**
+     * 记录ai事件token消耗日志
+     * @param content       事件名称
+     * @param eventType     事件类型(1文字 2图片 3...)
+     * @param tokenCount    token数量
+     * @param tokenType     token类型(1输入 2输出)
+     * @param user
+     */
+    public static void recordEventTokenLog(String content,Long senderId,Integer eventType, Long tokenCount, Integer tokenType, CompanyUser user) {
+        FastGptEventTokenLog fastGptEventTokenLog = new FastGptEventTokenLog();
+        fastGptEventTokenLog.setEventName(content);
+        fastGptEventTokenLog.setSenderId(senderId);
+        fastGptEventTokenLog.setRoleId(user.getFastgptRoleId());
+        fastGptEventTokenLog.setEventType(eventType);
+        fastGptEventTokenLog.setTokenCount(tokenCount);
+        fastGptEventTokenLog.setTokenType(tokenType);
+        fastGptEventTokenLog.setCompanyId(user.getCompanyId());
+        fastGptEventTokenLog.setCompanyUserId(user.getUserId());
+        fastGptEventTokenLog.setCreateTime(new Date());
+
+        EventLogQueue.addEventTokenLog(fastGptEventTokenLog); // 入队
+        //fastGptChatMsgService.insertFastGptEventTokenLog(fastGptEventTokenLog);
+    }
 
     public static void createEventTokenLog(String content,QwUser user,Long senderId,ChatDetailTStreamFResult result) {
 
@@ -150,5 +174,46 @@ public class EventLogUtils {
         Long completionTokens = (long) result.getUsage().getCompletion_tokens();
         recordEventTokenLog(content + "输出token",senderId,2,completionTokens,2, user);
     }
+    public static void createEventTokenLog(String content, CompanyUser user, Long senderId, AiImgResult result) {
+
+        Long totalTokens = (long) result.getUsage().getTotal_tokens();
+        recordEventTokenLog(content + "消耗token",senderId,2,totalTokens,0, user);
+
+        Long promptTokens = (long) result.getUsage().getPrompt_tokens();
+        recordEventTokenLog(content + "输入token",senderId,2,promptTokens,1, user);
+
+        Long completionTokens = (long) result.getUsage().getCompletion_tokens();
+        recordEventTokenLog(content + "输出token",senderId,2,completionTokens,2, user);
+    }
+    /**
+     *  记录ai事件日志
+     * @param count     触发数量
+     * @param type      事件类型(1互动 1总对话 3转人工 4AI无法回复转人工 5AI回复不合适转人工 6完课回复 7物流事件 8图片回复 9自定义事件回复 10用户未回复AI再次提醒)
+     * @param user      企微用户
+     */
+    public static void recordEventLog(Long senderId, Long count, Integer type, CompanyUser user) {
+        List<SysDictData> dictCache = getDictCache("sys_fastgpt_event_log_type");
+        String content = "未知事件";
+        if(dictCache != null){
+            for (SysDictData sysDictData : dictCache) {
+                if (type.toString().equals(sysDictData.getDictValue())){
+                    content = sysDictData.getDictLabel();
+                }
+            }
+        }
+        FastGptEventLog fastGptEventLog = new FastGptEventLog();
+        fastGptEventLog.setEventName(content);
+        fastGptEventLog.setSenderId(senderId);
+        fastGptEventLog.setRoleId(user.getFastgptRoleId());
+        fastGptEventLog.setCount(count);
+        fastGptEventLog.setType(type);
+        fastGptEventLog.setCompanyId(user.getCompanyId());
+        fastGptEventLog.setCompanyUserId(user.getUserId());
+        fastGptEventLog.setCreateTime(new Date());
+
+
+        EventLogQueue.addEventLog(fastGptEventLog); // 入队
+        //fastGptChatMsgService.insertFastGptEventLog(fastGptEventLog);
+    }
 
 }

+ 11 - 0
fs-service/src/main/java/com/fs/his/mapper/FsInquiryOrderMsgMapper.java

@@ -86,4 +86,15 @@ public interface FsInquiryOrderMsgMapper
             " order by m.msg_id asc" +
             "</script>"})
     List<FsInquiryOrderMsgListDVO> selectFsInquiryOrderMsgListDVO(@Param("maps")FsInquiryOrderMsgListDParam param);
+
+    /**
+     * 查询用户之间的历史消息记录
+     * @param sendID 发送方ID
+     * @param recvID 接收方ID
+     * @return 消息记录列表
+     */
+    @Select("SELECT * FROM fs_inquiry_order_msg " +
+            "WHERE (from_account = #{sendID} AND company_user_account = #{recvID}) " +
+            "ORDER BY create_time desc limit 5")
+    List<FsInquiryOrderMsg> selectHistoryMsgByUserId(@Param("sendID") String sendID,@Param("recvID")  String recvID);
 }

+ 65 - 0
fs-service/src/main/java/com/fs/his/service/IAppChatHistoryService.java

@@ -0,0 +1,65 @@
+package com.fs.his.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.app.chat.domain.AppChatHistory;
+import com.fs.im.vo.OpenImMsgCallBackVO;
+
+import java.util.List;
+
+/**
+ * app-聊天记录Service接口
+ *
+ * @author fs
+ * @date 2026-03-27
+ */
+public interface IAppChatHistoryService extends IService<AppChatHistory> {
+    /**
+     * 查询app-聊天记录
+     *
+     * @param id app-聊天记录主键
+     * @return app-聊天记录
+     */
+    AppChatHistory selectAppChatHistoryById(Long id);
+
+    /**
+     * 查询app-聊天记录列表
+     *
+     * @param appChatHistory app-聊天记录
+     * @return app-聊天记录集合
+     */
+    List<AppChatHistory> selectAppChatHistoryList(AppChatHistory appChatHistory);
+
+    /**
+     * 新增app-聊天记录
+     *
+     * @param appChatHistory app-聊天记录
+     * @return 结果
+     */
+    int insertAppChatHistory(AppChatHistory appChatHistory);
+
+    /**
+     * 修改app-聊天记录
+     *
+     * @param appChatHistory app-聊天记录
+     * @return 结果
+     */
+    int updateAppChatHistory(AppChatHistory appChatHistory);
+
+    /**
+     * 批量删除app-聊天记录
+     *
+     * @param ids 需要删除的app-聊天记录主键集合
+     * @return 结果
+     */
+    int deleteAppChatHistoryByIds(Long[] ids);
+
+    /**
+     * 删除app-聊天记录信息
+     *
+     * @param id app-聊天记录主键
+     * @return 结果
+     */
+    int deleteAppChatHistoryById(Long id);
+
+    void saveChatHistory(OpenImMsgCallBackVO messageInfo);
+}

+ 2 - 0
fs-service/src/main/java/com/fs/his/service/IFsInquiryOrderMsgService.java

@@ -71,4 +71,6 @@ public interface IFsInquiryOrderMsgService
     List<FsInquiryOrderMsgListDVO> selectFsInquiryOrderMsgListDVO(FsInquiryOrderMsgListDParam param);
 
     OpenImMsgCallBackResponse openImSaveMsg(OpenImMsgCallBackVO messageInfo) throws JsonProcessingException;
+
+    List<FsInquiryOrderMsg> selectHistoryMsgByUserId(String sendID, String recvID);
 }

+ 102 - 0
fs-service/src/main/java/com/fs/his/service/OpenImAsyncService.java

@@ -0,0 +1,102 @@
+package com.fs.his.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fs.app.chat.service.IAppChatHistoryService;
+import com.fs.common.utils.StringUtils;
+import com.fs.im.service.OpenIMService;
+import com.fs.im.vo.OpenImMsgCallBackVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.retry.annotation.Backoff;
+import org.springframework.retry.annotation.Recover;
+import org.springframework.retry.annotation.Retryable;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+@Service
+@Slf4j
+public class OpenImAsyncService {
+
+    @Resource
+    private OpenIMService openIMService;
+
+    @Lazy
+    @Autowired
+    private IFsInquiryOrderMsgService inquiryOrderMsgService;
+
+    @Autowired
+    private IAppChatHistoryService appChatHistoryService;
+
+    @Async("openImExecutor")
+    @Retryable(
+            value = Exception.class,       // 捕获所有异常
+            maxAttempts = 3,               // 最多重试 3 次
+            backoff = @Backoff(delay = 5000) // 重试间隔 5 秒
+    )
+    public void sendAsync(
+            String sendID,
+            String recvID,
+            Integer contentType,
+            String payloadData,
+            String diagnose,
+            String title,
+            String followId,
+            String orderId,
+            String ex
+    ) throws JsonProcessingException {
+        try {
+            openIMService.sendUtil(
+                    sendID, recvID, contentType,
+                    payloadData, diagnose, title,
+                    followId, orderId, ex
+            );
+        } catch (Exception e) {
+            log.error(
+                    "[sendAsync] IM发送失败 sendID={} recvID={} contentType={} orderId={}",
+                    sendID, recvID, contentType, orderId, e
+            );
+            throw e;
+        }
+    }
+
+    @Recover
+    public void recover(Exception e,
+                        String sendID,
+                        String recvID,
+                        Integer contentType,
+                        String payloadData,
+                        String diagnose,
+                        String title,
+                        String followId,
+                        String orderId,
+                        String ex) {
+        log.error("[sendAsync][recover] IM发送最终失败 sendID={} recvID={} orderId={}",
+                sendID, recvID, orderId, e);
+    }
+
+    @Async("openImExecutor")
+    public void handleCallback(OpenImMsgCallBackVO messageInfo) {
+        if (messageInfo == null || StringUtils.isBlank(messageInfo.getSendID())) {
+            log.warn("OpenIM 回调参数无效,跳过处理");
+            return;
+        }
+        try {
+            openIMService.AiAutoReply(copy(messageInfo));
+            appChatHistoryService.saveChatHistory(copy(messageInfo));
+            inquiryOrderMsgService.openImSaveMsg(copy(messageInfo));
+        } catch (Exception e) {
+            log.error("OpenIM 回调异步处理失败, sendID={}, recvID={}, clientMsgID={}",
+                    messageInfo.getSendID(), messageInfo.getRecvID(), messageInfo.getClientMsgID(), e);
+        }
+    }
+
+    private static OpenImMsgCallBackVO copy(OpenImMsgCallBackVO source) {
+        OpenImMsgCallBackVO target = new OpenImMsgCallBackVO();
+        BeanUtils.copyProperties(source, target);
+        return target;
+    }
+}

+ 8 - 1
fs-service/src/main/java/com/fs/his/service/impl/FsInquiryOrderMsgServiceImpl.java

@@ -338,13 +338,16 @@ public class FsInquiryOrderMsgServiceImpl implements IFsInquiryOrderMsgService {
         OpenImMsgCallBackResponse openImMsgCallBackResponse = new OpenImMsgCallBackResponse();
         ObjectMapper objectMapper = new ObjectMapper();
         try {
-            if (openImMsgCallBackVO.getCallbackCommand().equals("callbackBeforeAfterMsgCommand")) {
+            if ("callbackBeforeAfterMsgCommand".equals(openImMsgCallBackVO.getCallbackCommand())) {
                 fsInquiryOrderMsgMapper.deleteFsInquiryOrderMsgByMsgKey(openImMsgCallBackVO.getClientMsgID());
                 return openImMsgCallBackResponse;
             }
             String send = openImMsgCallBackVO.getSendID();
             String to = openImMsgCallBackVO.getRecvID();
             Long time = openImMsgCallBackVO.getSendTime();
+            if (time == null) {
+                time = System.currentTimeMillis();
+            }
             Date date = new Date(time);
             String content = openImMsgCallBackVO.getContent();
 
@@ -697,4 +700,8 @@ public class FsInquiryOrderMsgServiceImpl implements IFsInquiryOrderMsgService {
 
     }
 
+    @Override
+    public List<FsInquiryOrderMsg> selectHistoryMsgByUserId(String sendID, String recvID) {
+        return fsInquiryOrderMsgMapper.selectHistoryMsgByUserId(sendID, recvID);
+    }
 }

+ 89 - 0
fs-service/src/main/java/com/fs/im/domain/ImChatMsg.java

@@ -0,0 +1,89 @@
+package com.fs.im.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+/**
+ * 聊天记录对象 fastgpt_chat_msg
+ *
+ * @author fs
+ * @date 2024-10-10
+ */
+@Data
+public class ImChatMsg extends BaseEntity
+{
+    private static final long serialVersionUID = 1L;
+
+    /** $column.columnComment */
+    private Long msgId;
+
+    /** 消息id */
+    @Excel(name = "消息id")
+    private Long sessionId;
+
+    /**
+     * 用户id(用户类型为用户就是用户id,是客服就是存的客服id)
+     */
+    @Excel(name = "用户id")
+    private String userId;
+
+    /**
+     * 消息内容(完整消息,含结构体)
+     */
+    @Excel(name = "消息内容")
+    private String content;
+
+    /** 消息类型 1文本 */
+    @Excel(name = "消息类型 1文本")
+    private Integer msgType;
+
+    /** 公司ID */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /** 角色ID */
+    @Excel(name = "角色ID")
+    private Long roleId;
+
+    /** 客服ID */
+    @Excel(name = "客服ID")
+    private Long companyUserId;
+
+    /** 消息JSON */
+    @Excel(name = "消息JSON")
+    private String msgJson;
+
+    /** $column.columnComment */
+    @Excel(name = "消息JSON")
+    private Integer status;
+
+    /** 昵称 */
+    @Excel(name = "昵称")
+    private String nickName;
+
+    /** $column.columnComment */
+    @Excel(name = "昵称")
+    private String avatar;
+
+    /**
+     * 客服角色id(暂时未用上)
+     */
+    private Long appRoleId;
+
+    /**
+     * 用户类型,1-用户,2-销售
+     */
+    private Integer userType;
+
+    /**
+     * 消息内容(只含本次发送的内容)
+     */
+    private String shortContent;
+
+    /**
+     * 消息来源:0-AI客服聊天,为空则是原本聊天逻辑
+     */
+    private Integer sourceFrom;
+
+}

+ 121 - 0
fs-service/src/main/java/com/fs/im/domain/ImChatSession.java

@@ -0,0 +1,121 @@
+package com.fs.im.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * 对话关系对象 fastgpt_chat_session
+ *
+ * @author fs
+ * @date 2024-10-10
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ImChatSession extends BaseEntity {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 会话ID
+     */
+    @TableId
+    private Long sessionId;
+
+    /**
+     * 聊天id
+     */
+    @Excel(name = "聊天id")
+    private String chatId;
+
+    /**
+     * 客户ID uid
+     */
+    @Excel(name = "客户ID uid")
+    private String userId;
+
+    /**
+     * 客服ID 应用id?
+     */
+    @Excel(name = "客服ID 应用id?")
+    private String kfId;
+
+    /**
+     * 公司ID
+     */
+    @Excel(name = "公司ID")
+    private Long companyId;
+
+    /**
+     * 客户昵称
+     */
+    @Excel(name = "客户昵称")
+    private String nickName;
+
+    /**
+     * 头像
+     */
+    @Excel(name = "头像")
+    private String avatar;
+
+    private Long qwUserId;
+
+    private String userName;
+    //1人工,2ai
+    private Integer personOrAi;
+
+    /**
+     * 学习到的章节
+     */
+    private String study;
+
+    /**
+     * 今日课程完成情况
+     */
+    private String courseStatus;
+
+    /**
+     * 课程名称
+     */
+    private String courseName;
+
+    /**
+     * 课节名称
+     */
+    private String videoName;
+
+    /**
+     * 最近交流时间(为空则是用户首次发起对话)
+     */
+    private Date lastTalkTime;
+
+    /**
+     * 交流状态
+     */
+    @Excel(name = "交流状态")
+    private String talk;
+
+    /**
+     * 用户自动触达AI状态,0-已关闭,1-已开启
+     */
+    private Integer remindStatus;
+
+    /**
+     * 触达时间,当且仅当触达状态开启时有效
+     */
+    private Date remindTime;
+
+    /**
+     * 触达累计次数
+     */
+    private Integer remindCount;
+
+    /**
+     * 客户和销售对话中实际收集到的信息
+     */
+    private String userInfo;
+
+}

+ 12 - 0
fs-service/src/main/java/com/fs/im/dto/OpenImMsgCallBackResponse.java

@@ -9,4 +9,16 @@ public class OpenImMsgCallBackResponse {
     private String errMsg = "";
     private String errDlt = "";
     private Integer nextCode = 0;
+
+    public static OpenImMsgCallBackResponse success() {
+        return new OpenImMsgCallBackResponse();
+    }
+
+    public static OpenImMsgCallBackResponse fail(String errMsg) {
+        OpenImMsgCallBackResponse response = new OpenImMsgCallBackResponse();
+        response.setActionCode(202);
+        response.setErrCode(100);
+        response.setErrMsg(errMsg);
+        return response;
+    }
 }

+ 65 - 0
fs-service/src/main/java/com/fs/im/mapper/ImChatMsgMapper.java

@@ -0,0 +1,65 @@
+package com.fs.im.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.im.domain.ImChatMsg;
+
+import java.util.List;
+
+
+/**
+ * 聊天消息Mapper接口
+ *
+ * @author fs
+ * @date 2025-08-18
+ */
+public interface ImChatMsgMapper extends BaseMapper<ImChatMsg>{
+    /**
+     * 查询聊天消息
+     *
+     * @param msgId 聊天消息主键
+     * @return 聊天消息
+     */
+    ImChatMsg selectImChatMsgByMsgId(Long msgId);
+
+    /**
+     * 查询聊天消息列表
+     *
+     * @param imChatMsg 聊天消息
+     * @return 聊天消息集合
+     */
+    List<ImChatMsg> selectImChatMsgList(ImChatMsg imChatMsg);
+
+    List<ImChatMsg> getListBySessionId(Long sessionId);
+
+    /**
+     * 新增聊天消息
+     *
+     * @param imChatMsg 聊天消息
+     * @return 结果
+     */
+    int insertImChatMsg(ImChatMsg imChatMsg);
+
+    /**
+     * 修改聊天消息
+     *
+     * @param imChatMsg 聊天消息
+     * @return 结果
+     */
+    int updateImChatMsg(ImChatMsg imChatMsg);
+
+    /**
+     * 删除聊天消息
+     *
+     * @param msgId 聊天消息主键
+     * @return 结果
+     */
+    int deleteImChatMsgByMsgId(Long msgId);
+
+    /**
+     * 批量删除聊天消息
+     *
+     * @param msgIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteImChatMsgByMsgIds(Long[] msgIds);
+}

+ 70 - 0
fs-service/src/main/java/com/fs/im/mapper/ImChatSessionMapper.java

@@ -0,0 +1,70 @@
+package com.fs.im.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.im.domain.ImChatSession;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 聊天会话Mapper接口
+ *
+ * @author fs
+ * @date 2025-08-18
+ */
+public interface ImChatSessionMapper extends BaseMapper<ImChatSession>{
+    /**
+     * 查询聊天会话
+     *
+     * @param sessionId 聊天会话主键
+     * @return 聊天会话
+     */
+    ImChatSession selectImChatSessionBySessionId(Long sessionId);
+
+    /**
+     * 查询聊天会话列表
+     *
+     * @param imChatSession 聊天会话
+     * @return 聊天会话集合
+     */
+    List<ImChatSession> selectImChatSessionList(ImChatSession imChatSession);
+
+    /**
+     * 新增聊天会话
+     *
+     * @param imChatSession 聊天会话
+     * @return 结果
+     */
+    int insertImChatSession(ImChatSession imChatSession);
+
+    /**
+     * 修改聊天会话
+     *
+     * @param imChatSession 聊天会话
+     * @return 结果
+     */
+    int updateImChatSession(ImChatSession imChatSession);
+
+    /**
+     * 删除聊天会话
+     *
+     * @param sessionId 聊天会话主键
+     * @return 结果
+     */
+    int deleteImChatSessionBySessionId(Long sessionId);
+
+    /**
+     * 批量删除聊天会话
+     *
+     * @param sessionIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteImChatSessionBySessionIds(Long[] sessionIds);
+
+    @Select("select * from im_chat_session where chat_id = #{conversationId}")
+    ImChatSession selectImChatSessionByChatId(@Param("conversationId") String conversationId);
+
+    @Select("select * from im_chat_session where company_id = #{companyId} and user_id = #{userId} limit 1")
+    ImChatSession selectByCompanyIdAndUserId(@Param("companyId") Long companyId, @Param("userId") String userId);
+}

+ 64 - 0
fs-service/src/main/java/com/fs/im/service/IImChatMsgService.java

@@ -0,0 +1,64 @@
+package com.fs.im.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.im.domain.ImChatMsg;
+
+import java.util.List;
+
+/**
+ * 聊天消息Service接口
+ *
+ * @author fs
+ * @date 2025-08-18
+ */
+public interface IImChatMsgService extends IService<ImChatMsg>{
+    /**
+     * 查询聊天消息
+     *
+     * @param msgId 聊天消息主键
+     * @return 聊天消息
+     */
+    ImChatMsg selectImChatMsgByMsgId(Long msgId);
+
+    List<ImChatMsg> getListBySessionId(Long sessionId);
+
+    /**
+     * 查询聊天消息列表
+     *
+     * @param imChatMsg 聊天消息
+     * @return 聊天消息集合
+     */
+    List<ImChatMsg> selectImChatMsgList(ImChatMsg imChatMsg);
+
+    /**
+     * 新增聊天消息
+     *
+     * @param imChatMsg 聊天消息
+     * @return 结果
+     */
+    int insertImChatMsg(ImChatMsg imChatMsg);
+
+    /**
+     * 修改聊天消息
+     *
+     * @param imChatMsg 聊天消息
+     * @return 结果
+     */
+    int updateImChatMsg(ImChatMsg imChatMsg);
+
+    /**
+     * 批量删除聊天消息
+     *
+     * @param msgIds 需要删除的聊天消息主键集合
+     * @return 结果
+     */
+    int deleteImChatMsgByMsgIds(Long[] msgIds);
+
+    /**
+     * 删除聊天消息信息
+     *
+     * @param msgId 聊天消息主键
+     * @return 结果
+     */
+    int deleteImChatMsgByMsgId(Long msgId);
+}

+ 66 - 0
fs-service/src/main/java/com/fs/im/service/IImChatSessionService.java

@@ -0,0 +1,66 @@
+package com.fs.im.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.im.domain.ImChatSession;
+
+import java.util.List;
+
+/**
+ * 聊天会话Service接口
+ *
+ * @author fs
+ * @date 2025-08-18
+ */
+public interface IImChatSessionService extends IService<ImChatSession>{
+    /**
+     * 查询聊天会话
+     *
+     * @param sessionId 聊天会话主键
+     * @return 聊天会话
+     */
+    ImChatSession selectImChatSessionBySessionId(Long sessionId);
+
+    /**
+     * 查询聊天会话列表
+     *
+     * @param imChatSession 聊天会话
+     * @return 聊天会话集合
+     */
+    List<ImChatSession> selectImChatSessionList(ImChatSession imChatSession);
+
+    /**
+     * 新增聊天会话
+     *
+     * @param imChatSession 聊天会话
+     * @return 结果
+     */
+    int insertImChatSession(ImChatSession imChatSession);
+
+    /**
+     * 修改聊天会话
+     *
+     * @param imChatSession 聊天会话
+     * @return 结果
+     */
+    int updateImChatSession(ImChatSession imChatSession);
+
+    /**
+     * 批量删除聊天会话
+     *
+     * @param sessionIds 需要删除的聊天会话主键集合
+     * @return 结果
+     */
+    int deleteImChatSessionBySessionIds(Long[] sessionIds);
+
+    /**
+     * 删除聊天会话信息
+     *
+     * @param sessionId 聊天会话主键
+     * @return 结果
+     */
+    int deleteImChatSessionBySessionId(Long sessionId);
+
+    ImChatSession selectImChatSessionByChatId(String conversationId);
+
+    ImChatSession selectByCompanyIdAndUserId(Long companyId, String userId);
+}

+ 2 - 0
fs-service/src/main/java/com/fs/im/service/OpenIMService.java

@@ -128,4 +128,6 @@ public interface OpenIMService {
      * @return 发送结果
      */
      OpenImResponseDTO batchSendTextMessage(String senderId, List<String> receiverIds, String textContent, String pushTitle, String pushDesc, String ex);
+
+    int cancleAiToPerson(String chatId);
 }

+ 99 - 0
fs-service/src/main/java/com/fs/im/service/impl/ImChatMsgServiceImpl.java

@@ -0,0 +1,99 @@
+package com.fs.im.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.im.domain.ImChatMsg;
+import com.fs.im.mapper.ImChatMsgMapper;
+import com.fs.im.service.IImChatMsgService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 聊天消息Service业务层处理
+ *
+ * @author fs
+ * @date 2025-08-18
+ */
+@Service
+public class ImChatMsgServiceImpl extends ServiceImpl<ImChatMsgMapper, ImChatMsg> implements IImChatMsgService {
+
+    /**
+     * 查询聊天消息
+     *
+     * @param msgId 聊天消息主键
+     * @return 聊天消息
+     */
+    @Override
+    public ImChatMsg selectImChatMsgByMsgId(Long msgId)
+    {
+        return baseMapper.selectImChatMsgByMsgId(msgId);
+    }
+
+    @Override
+    public List<ImChatMsg> getListBySessionId(Long sessionId) {
+        return baseMapper.getListBySessionId(sessionId);
+    }
+
+    /**
+     * 查询聊天消息列表
+     *
+     * @param imChatMsg 聊天消息
+     * @return 聊天消息
+     */
+    @Override
+    public List<ImChatMsg> selectImChatMsgList(ImChatMsg imChatMsg)
+    {
+        return baseMapper.selectImChatMsgList(imChatMsg);
+    }
+
+    /**
+     * 新增聊天消息
+     *
+     * @param imChatMsg 聊天消息
+     * @return 结果
+     */
+    @Override
+    public int insertImChatMsg(ImChatMsg imChatMsg)
+    {
+        imChatMsg.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertImChatMsg(imChatMsg);
+    }
+
+    /**
+     * 修改聊天消息
+     *
+     * @param imChatMsg 聊天消息
+     * @return 结果
+     */
+    @Override
+    public int updateImChatMsg(ImChatMsg imChatMsg)
+    {
+        imChatMsg.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateImChatMsg(imChatMsg);
+    }
+
+    /**
+     * 批量删除聊天消息
+     *
+     * @param msgIds 需要删除的聊天消息主键
+     * @return 结果
+     */
+    @Override
+    public int deleteImChatMsgByMsgIds(Long[] msgIds)
+    {
+        return baseMapper.deleteImChatMsgByMsgIds(msgIds);
+    }
+
+    /**
+     * 删除聊天消息信息
+     *
+     * @param msgId 聊天消息主键
+     * @return 结果
+     */
+    @Override
+    public int deleteImChatMsgByMsgId(Long msgId)
+    {
+        return baseMapper.deleteImChatMsgByMsgId(msgId);
+    }
+}

+ 104 - 0
fs-service/src/main/java/com/fs/im/service/impl/ImChatSessionServiceImpl.java

@@ -0,0 +1,104 @@
+package com.fs.im.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.utils.DateUtils;
+import com.fs.im.domain.ImChatSession;
+import com.fs.im.mapper.ImChatSessionMapper;
+import com.fs.im.service.IImChatSessionService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 聊天会话Service业务层处理
+ *
+ * @author fs
+ * @date 2025-08-18
+ */
+@Service
+public class ImChatSessionServiceImpl extends ServiceImpl<ImChatSessionMapper, ImChatSession> implements IImChatSessionService {
+
+    /**
+     * 查询聊天会话
+     *
+     * @param sessionId 聊天会话主键
+     * @return 聊天会话
+     */
+    @Override
+    public ImChatSession selectImChatSessionBySessionId(Long sessionId)
+    {
+        return baseMapper.selectImChatSessionBySessionId(sessionId);
+    }
+
+    /**
+     * 查询聊天会话列表
+     *
+     * @param imChatSession 聊天会话
+     * @return 聊天会话
+     */
+    @Override
+    public List<ImChatSession> selectImChatSessionList(ImChatSession imChatSession)
+    {
+        return baseMapper.selectImChatSessionList(imChatSession);
+    }
+
+    /**
+     * 新增聊天会话
+     *
+     * @param imChatSession 聊天会话
+     * @return 结果
+     */
+    @Override
+    public int insertImChatSession(ImChatSession imChatSession)
+    {
+        imChatSession.setCreateTime(DateUtils.getNowDate());
+        return baseMapper.insertImChatSession(imChatSession);
+    }
+
+    /**
+     * 修改聊天会话
+     *
+     * @param imChatSession 聊天会话
+     * @return 结果
+     */
+    @Override
+    public int updateImChatSession(ImChatSession imChatSession)
+    {
+        imChatSession.setUpdateTime(DateUtils.getNowDate());
+        return baseMapper.updateImChatSession(imChatSession);
+    }
+
+    /**
+     * 批量删除聊天会话
+     *
+     * @param sessionIds 需要删除的聊天会话主键
+     * @return 结果
+     */
+    @Override
+    public int deleteImChatSessionBySessionIds(Long[] sessionIds)
+    {
+        return baseMapper.deleteImChatSessionBySessionIds(sessionIds);
+    }
+
+    /**
+     * 删除聊天会话信息
+     *
+     * @param sessionId 聊天会话主键
+     * @return 结果
+     */
+    @Override
+    public int deleteImChatSessionBySessionId(Long sessionId)
+    {
+        return baseMapper.deleteImChatSessionBySessionId(sessionId);
+    }
+
+    @Override
+    public ImChatSession selectImChatSessionByChatId(String conversationId) {
+        return baseMapper.selectImChatSessionByChatId(conversationId);
+    }
+
+    @Override
+    public ImChatSession selectByCompanyIdAndUserId(Long companyId, String userId) {
+        return baseMapper.selectByCompanyIdAndUserId(companyId, userId);
+    }
+}

+ 152 - 14
fs-service/src/main/java/com/fs/im/service/impl/OpenIMServiceImpl.java

@@ -37,11 +37,13 @@ import com.fs.his.mapper.FsUserMapper;
 import com.fs.im.config.IMConfig;
 import com.fs.im.domain.FsImMsgSendDetail;
 import com.fs.im.domain.FsImMsgSendLog;
+import com.fs.im.domain.ImChatSession;
 import com.fs.im.domain.ImSendLog;
 import com.fs.im.dto.*;
 import com.fs.im.mapper.FsImMsgSendDetailMapper;
 import com.fs.im.mapper.FsImMsgSendLogMapper;
 import com.fs.im.mapper.ImSendLogMapper;
+import com.fs.im.service.IImChatSessionService;
 import com.fs.im.service.OpenIMService;
 import com.fs.im.vo.OpenImMsgCallBackVO;
 import com.fs.im.vo.OpenImResponseDTOTest;
@@ -58,6 +60,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
@@ -79,6 +82,11 @@ public class OpenIMServiceImpl implements OpenIMService {
     /** HTTP请求超时时间(毫秒) */
     private static final int HTTP_TIMEOUT_MS = 60000;
 
+    @Autowired
+    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
+    @Autowired
+    private IImChatSessionService imChatSessionService;
+
     @Autowired
     IMConfig imConfig;
     @Autowired
@@ -171,7 +179,13 @@ public class OpenIMServiceImpl implements OpenIMService {
 
     @Override
     public OpenImResponseDTO AiAutoReply(OpenImMsgCallBackVO openImMsgDTO) throws JsonProcessingException {
+
         try {
+            // 检查是否包含转人工
+            if (checkImMsgContent(openImMsgDTO)) {
+                return null;
+            }
+
             String sendID = openImMsgDTO.getSendID();
             // 如果发送人不是用户 直接返回 不做自动回复
             if (!sendID.startsWith("U")) {
@@ -184,18 +198,13 @@ public class OpenIMServiceImpl implements OpenIMService {
             // 初始化消息DTO对象
             OpenImMsgDTO replyMsg = new OpenImMsgDTO();
             String recvID = openImMsgDTO.getRecvID();
-            Long companyId = 0L;
+//            Long companyId = 0L;
+            CompanyUser company = null;
             if (recvID.startsWith("D")) {
                 // 因为主要是销售想接入ai ,所以医生不回复,只回复销售 下面暂时保留
                 return null;
-//                FsDoctor fsDoctor = fsDoctorMapper.selectFsDoctorByDoctorId(Long.parseLong(recvID.replace("D", "")));
-//                if (null != fsDoctor && StringUtils.isNotEmpty(fsDoctor.getAvatar())) {
-//                    offlinePushInfo.setTitle(fsDoctor.getDoctorName());
-//                    replyMsg.setSenderFaceURL(fsDoctor.getAvatar());
-//                }
             } else if (recvID.startsWith("C")) {
-                CompanyUser company = companyUserMapper.selectCompanyUserByUserId(Long.parseLong(recvID.replace("C", "")));
-                companyId = company.getCompanyId();
+                company = companyUserMapper.selectCompanyUserByUserId(Long.parseLong(recvID.replace("C", "")));
                 if (null != company && StringUtils.isNotEmpty(company.getAvatar())) {
                     offlinePushInfo.setTitle(company.getImNickName());
                     replyMsg.setSenderFaceURL(company.getAvatar());
@@ -204,11 +213,20 @@ public class OpenIMServiceImpl implements OpenIMService {
             }
             // 对接收者ID进行账户校验
             accountCheck(recvID.toString(), recvType);
-            if (companyId == 0L) {
-                return null;
+            if (ObjectUtils.isEmpty(company)) {
+               return null;
+            } else {
+                // 没有CompanyUser的场景,使用FsUser走新的AiReply逻辑
+                FsUser fsUser = fsUserMapper.selectFsUserByUserId(Long.parseLong(sendID.replace("U", "")));
+                if (fsUser == null) {
+                    log.error("AiAutoReply: 未找到FsUser, sendID={}", sendID);
+                    return null;
+                }
+                OpenImMsgCallBackVO openImMsgCallBackVO = new OpenImMsgCallBackVO();
+                BeanUtils.copyProperties(openImMsgDTO, openImMsgCallBackVO);
+                threadPoolTaskExecutor.execute(() -> aiHookService.AiReply(openImMsgCallBackVO, fsUser));
             }
-            // ai接口智能回复
-            R r = aiHookService.AiReply(openImMsgDTO, companyId);
+       /*     R r = aiHookService.AiReply(openImMsgDTO, companyId);
             if (r.get("code").equals(500)) {
                 return null;
             }
@@ -256,7 +274,7 @@ public class OpenIMServiceImpl implements OpenIMService {
                 openImEditConversationDTO.setConversation(openImConversationDTO);
                 OpenImResponseDTO openImResponseDTO1 = editConversation(openImEditConversationDTO);
                 log.debug("修改回话返回参数:{}", openImResponseDTO1);
-            }
+            }*/
             return null;
         } catch (Exception e) {
             log.error("openImMsgCallBack,自动销售回复消息,发送消息失败:", e);
@@ -266,7 +284,69 @@ public class OpenIMServiceImpl implements OpenIMService {
 
     }
 
+    private static final String HUMAN_KEY_PREFIX = "IM:SESSION:HUMAN:";
+
     private Boolean checkImMsgContent(OpenImMsgCallBackVO openImMsgDTO) {
+
+        String content = openImMsgDTO.getContent();
+        if (content == null || content.trim().isEmpty()) {
+            return true;
+        }
+
+        String conversationId = "si_" + openImMsgDTO.getRecvID() + "_" + openImMsgDTO.getSendID();
+        String humanKey = HUMAN_KEY_PREFIX + conversationId;
+
+        // 查询当前会话
+        ImChatSession session = imChatSessionService.selectImChatSessionByChatId(conversationId);
+
+        // 不存在 → 默认AI
+        if (session == null) {
+            // 如果是用户发送消息
+            if (openImMsgDTO.getSendID().startsWith("U")){
+                ImChatSession insert = new ImChatSession();
+                insert.setChatId(conversationId);
+                insert.setUserId(stripPrefix(openImMsgDTO.getSendID(), "U"));
+                insert.setKfId(stripPrefix(openImMsgDTO.getRecvID(), "C"));
+                insert.setPersonOrAi(2); // 默认AI
+                insert.setCreateTime(new Date());
+                imChatSessionService.insertImChatSession(insert);
+            }
+            return false; // AI回复
+        }
+        if (session.getPersonOrAi() != null && session.getPersonOrAi() == 1) {
+
+            // 判断是否还在人工有效期内
+            Boolean exists = redisCache.hasKey(humanKey);
+
+            if (Boolean.TRUE.equals(exists)) {
+
+                // 刷新10分钟有效期(续命)
+                redisCache.setCacheObject(humanKey, "1", 10, TimeUnit.MINUTES);
+
+                return true; // 人工回复
+            } else {
+
+                // 超时 → 自动切回AI
+                session.setPersonOrAi(2);
+                imChatSessionService.updateImChatSession(session);
+
+                return false; // AI回复
+            }
+        }
+
+        // 默认AI
+        return false;
+    }
+
+    private String stripPrefix(String value, String prefix) {
+        if (org.apache.commons.lang3.StringUtils.isEmpty(value)) {
+            return value;
+        }
+        return value.startsWith(prefix) ? value.substring(1) : value;
+    }
+
+
+    /*private Boolean checkImMsgContent(OpenImMsgCallBackVO openImMsgDTO) {
         String content = openImMsgDTO.getContent();
         if (content == null || content.isEmpty() || content.trim().isEmpty()) {
             return true;
@@ -313,7 +393,7 @@ public class OpenIMServiceImpl implements OpenIMService {
             return true;
         }
     }
-
+*/
     private  String getIMList( String recvID,String  conversationId)  {
         // 优化:直接查询指定会话,而非分页遍历全部会话
         String adminToken = getAdminToken();
@@ -2390,4 +2470,62 @@ public class OpenIMServiceImpl implements OpenIMService {
             return errorResponse;
         }
     }
+
+
+    @Override
+    public int cancleAiToPerson(String chatId) {
+        ImChatSession imChatSession = imChatSessionService.selectImChatSessionByChatId(chatId);
+        String[] split = chatId.split("_");
+        if (split.length != 3) return -1;
+
+        String received = split[1].replace("C", "");
+        String sendId = split[2].replace("U", "");
+
+        try {
+            String ex = getIMList(split[2], chatId);
+
+            OpenImEditConversationDTO currentConversation = new OpenImEditConversationDTO();
+            ArrayList<String> userIDs = new ArrayList<>();
+            userIDs.add(split[2]);
+            currentConversation.setUserIDs(userIDs);
+
+            JSONObject exJson;
+            if (StringUtils.isNotEmpty(ex)) {
+                exJson = new JSONObject(ex);
+            } else {
+                exJson = new JSONObject();
+            }
+
+            if (exJson.has("IsArtificial")) {
+                String humanKey = HUMAN_KEY_PREFIX + imChatSession.getChatId();
+                // 写入Redis 10分钟有效期
+                redisCache.setCacheObject(humanKey, "1", 10, TimeUnit.MINUTES);
+                imChatSession.setPersonOrAi(1);
+                imChatSessionService.updateImChatSession(imChatSession);
+                // 已存在,说明是人工,取消标识
+                exJson.remove("IsArtificial");
+            } else {
+                // 不存在,补充人工标识
+                long tenMinutesLater = System.currentTimeMillis() + 10 * 60 * 1000;
+                exJson.put("IsArtificial", tenMinutesLater);
+                imChatSession.setPersonOrAi(2);
+                imChatSessionService.updateImChatSession(imChatSession);
+            }
+
+            OpenImConversationDTO openImConversationDTO = new OpenImConversationDTO();
+            openImConversationDTO.setConversationID(chatId);
+            openImConversationDTO.setConversationType(1);
+            openImConversationDTO.setUserID(received);
+            openImConversationDTO.setEx(exJson.toString());
+
+            currentConversation.setConversation(openImConversationDTO);
+            editConversation(currentConversation);
+            return 1;
+
+        } catch (Exception e) {
+            log.error("取消智能回复失败:", e);
+        }
+        return 0;
+    }
+
 }

+ 34 - 0
fs-service/src/main/java/com/fs/im/util/JsonUtil.java

@@ -0,0 +1,34 @@
+package com.fs.im.util;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class JsonUtil {
+
+    // 共享一个 ObjectMapper,配置只序列化非 null 字段
+    private static final ObjectMapper mapper = new ObjectMapper()
+            .setSerializationInclusion(JsonInclude.Include.NON_NULL);
+
+    /**
+     * 对象转 JSON 字符串,自动忽略 null 字段
+     */
+    public static String toJson(Object obj) {
+        try {
+            return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException("序列化对象为JSON失败", e);
+        }
+    }
+
+    /**
+     * JSON 转对象
+     */
+    public static <T> T fromJson(String json, Class<T> clazz) {
+        try {
+            return mapper.readValue(json, clazz);
+        } catch (Exception e) {
+            throw new RuntimeException("反序列化JSON失败", e);
+        }
+    }
+}

+ 316 - 0
fs-service/src/main/java/com/fs/im/util/OpenIMUtil.java

@@ -0,0 +1,316 @@
+package com.fs.im.util;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.his.dto.PayloadDTO;
+import com.fs.im.config.IMConfig;
+import com.fs.im.dto.OpenImConversationDTO;
+import com.fs.im.dto.OpenImEditConversationDTO;
+import com.fs.im.dto.OpenImMsgDTO;
+import com.fs.im.dto.OpenImResponseDTO;
+import com.fs.im.vo.OpenImMsgCallBackVO;
+import lombok.extern.slf4j.Slf4j;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+@Slf4j
+public class OpenIMUtil {
+
+    /**
+     * 检查IM消息内容,判断是否需要跳过自动回复
+     *
+     * @param openImMsgDTO 消息内容
+     * @param imConfig IM配置
+     * @param redisCache Redis缓存
+     * @return true-跳过自动回复,false-继续自动回复
+     */
+    public static Boolean checkImMsgContent(OpenImMsgCallBackVO openImMsgDTO, IMConfig imConfig, RedisCache redisCache) {
+        String content = openImMsgDTO.getContent();
+        if (content == null || content.isEmpty() || content.trim().isEmpty()) {
+            return true;
+        }
+        String conversationId = "si_" + openImMsgDTO.getRecvID() + "_" + openImMsgDTO.getSendID();
+        // 三种情况 ①正常聊天  ② 转人工 ③人工之后的聊天
+        // 检测内容是否包含
+        try {
+            //① 存在转人工配置 返回true 跳过自动回复
+            String ex = getIMList(openImMsgDTO.getSendID(), conversationId, imConfig, redisCache);
+            if (ex != null && ex.contains("IsArtificial")) {
+                return true;
+            }
+            //② 检查当前语句 是否包含转人工
+            if (content.contains("转人工")) {
+                OpenImEditConversationDTO currentConversation = new OpenImEditConversationDTO();
+                ArrayList<String> userIDs = new ArrayList<>();
+                userIDs.add(openImMsgDTO.getSendID());
+                currentConversation.setUserIDs(userIDs);
+                OpenImConversationDTO openImConversationDTO = new OpenImConversationDTO();
+                openImConversationDTO.setConversationID(conversationId);
+                openImConversationDTO.setConversationType(1);
+                openImConversationDTO.setUserID(openImMsgDTO.getRecvID());
+                // 不存在配置 直接放
+                if (ex == null || StringUtils.isEmpty(ex.trim())) {
+                    JSONObject jsonObject = new JSONObject();
+                    jsonObject.put("IsArtificial", 1);
+                    openImConversationDTO.setEx(jsonObject.toString());
+                } else {
+                    // 存在配置 添加之后再放
+                    JSONObject exJson = new JSONObject(ex);
+                    exJson.put("IsArtificial", 1);
+                    openImConversationDTO.setEx(exJson.toString());
+                }
+                currentConversation.setConversation(openImConversationDTO);
+                OpenImResponseDTO openImResponseDTO1 = editConversation(currentConversation, imConfig, redisCache);
+                log.info("修改回话返回参数:{}", openImResponseDTO1);
+                return true;
+            }
+            //③正常情况 直接返回
+            return false;
+        } catch (Exception e) {
+            log.error("openImMsgCallBack,检测内容是否包含转人工失败:", e);
+            return true;
+        }
+    }
+
+    /**
+     * 检测根据类型进行转人工
+     *
+     * @param openImMsgDTO
+     * @param imConfig
+     * @param redisCache
+     * @param type 1 设置为转人工 2. 设置为ai
+     * @return
+     */
+    public static void editConversationEx(OpenImMsgCallBackVO openImMsgDTO, IMConfig imConfig, RedisCache redisCache,Integer type) {
+        String conversationId = "si_" + openImMsgDTO.getRecvID() + "_" + openImMsgDTO.getSendID();
+        try {
+            String ex = getIMList(openImMsgDTO.getRecvID(), conversationId, imConfig, redisCache);
+            OpenImEditConversationDTO currentConversation = new OpenImEditConversationDTO();
+            ArrayList<String> userIDs = new ArrayList<>();
+            userIDs.add(openImMsgDTO.getSendID());
+            currentConversation.setUserIDs(userIDs);
+            OpenImConversationDTO openImConversationDTO = new OpenImConversationDTO();
+            openImConversationDTO.setConversationID(conversationId);
+            openImConversationDTO.setConversationType(1);
+            openImConversationDTO.setUserID(openImMsgDTO.getRecvID());
+            if (type == 1) {
+                Date tenMinutesLater = new Date(System.currentTimeMillis() + 10 * 60 * 1000);
+                // 不存在配置 直接放
+                if (ex == null || StringUtils.isEmpty(ex.trim())) {
+                    JSONObject jsonObject = new JSONObject();
+                    jsonObject.put("IsArtificial", tenMinutesLater.getTime());
+                    openImConversationDTO.setEx(jsonObject.toString());
+                } else {
+                    // 存在配置 添加之后再放
+                    JSONObject exJson = new JSONObject(ex);
+                    exJson.put("IsArtificial", tenMinutesLater.getTime());
+                    openImConversationDTO.setEx(exJson.toString());
+                }
+            } else if (type == 2) {
+                // 存在配置 删除掉
+                JSONObject exJson = new JSONObject(ex);
+                exJson.remove("IsArtificial");
+                openImConversationDTO.setEx(exJson.toString());
+            }
+            currentConversation.setConversation(openImConversationDTO);
+            editConversation(currentConversation, imConfig, redisCache);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    public static void sendIMMsg(CompanyUser companyUser, OpenImMsgCallBackVO openImMsgDTO, IMConfig imConfig, RedisCache redisCache) {
+        try {
+            String msg = openImMsgDTO.getContent();
+            if (msg == null || msg.trim().isEmpty()){
+                log.info("输出为空格");
+                return;
+            }
+            OpenImMsgDTO.OfflinePushInfo offlinePushInfo = new OpenImMsgDTO.OfflinePushInfo();
+            OpenImMsgDTO replyMsg = new OpenImMsgDTO();
+            if (null != companyUser && org.apache.commons.lang3.StringUtils.isNotEmpty(companyUser.getAvatar())) {
+                offlinePushInfo.setTitle(companyUser.getNickName());
+                replyMsg.setSenderFaceURL(companyUser.getAvatar());
+            }
+            // 初始化ObjectMapper对象,用于JSON序列化和反序列化
+            ObjectMapper objectMapper = new ObjectMapper();
+            // 设置消息的发送者ID、接收者ID、内容类型等基础信息
+            // 将发送者和接收者交换一下
+            replyMsg.setSendID(openImMsgDTO.getRecvID());
+            replyMsg.setRecvID(openImMsgDTO.getSendID());
+            replyMsg.setContentType(101);
+            replyMsg.setSenderPlatformID(openImMsgDTO.getSenderPlatformID());
+            replyMsg.setSessionType(openImMsgDTO.getSessionType());
+            // 初始化消息内容对象
+            OpenImMsgDTO.Content content = new OpenImMsgDTO.Content();
+            // 初始化负载数据对象,并设置数据内容
+            PayloadDTO payload = new PayloadDTO();
+            payload.setData(msg);
+            PayloadDTO.Extension extension = new PayloadDTO.Extension();
+            extension.setTitle("快速回复"); // 可选标题
+            payload.setExtension(extension);
+            OpenImMsgDTO.ImData imData = new OpenImMsgDTO.ImData();
+            imData.setPayload(payload);
+            String imJson = objectMapper.writeValueAsString(imData);
+            content.setData(imJson);
+            content.setContent(msg);
+            replyMsg.setContent(content);
+            offlinePushInfo.setDesc("快速回复");
+            offlinePushInfo.setIOSBadgeCount(true);
+            offlinePushInfo.setIOSPushSound("");
+            replyMsg.setOfflinePushInfo(offlinePushInfo);
+            // 发送消息
+            OpenImResponseDTO response = OpenIMUtil.openIMSendMsg(replyMsg, imConfig, redisCache);
+        }catch (Exception e ){
+            log.error("修改会话异常:{}",e);
+        }
+    }
+
+    public static OpenImResponseDTO openIMSendMsg(OpenImMsgDTO openImMsgDTO, IMConfig imConfig, RedisCache redisCache) {
+        log.info("进入发消息的方法");
+        String adminToken = getAdminToken(imConfig, redisCache);
+        JSONObject jsonObject = new JSONObject(openImMsgDTO);
+        log.info("发送消息的请求体:\n{}", jsonObject.toString());
+        long time = new Date().getTime();
+        String url = IMConfig.URL+"/msg/send_msg";
+        String result = HttpRequest.post(url)
+                .header("operationID", time + "")
+                .header("token",adminToken)
+                .body(jsonObject.toString())
+                .execute()
+                .body();
+        OpenImResponseDTO responseDTO= JSONUtil.toBean(result,OpenImResponseDTO.class);
+        log.info("发送消息返回内容:\n{}", responseDTO);
+        return responseDTO;
+    }
+
+    /**
+     * 获取IM列表
+     *
+     * @param recvID 接收者ID
+     * @param conversationId 会话ID
+     * @param imConfig IM配置
+     * @param redisCache Redis缓存
+     * @return 会话扩展信息
+     */
+    private static String getIMList(String recvID, String conversationId, IMConfig imConfig, RedisCache redisCache) {
+        int pageNumber = 1;
+        int pageSize = 20;
+        int maxPages = 10; // 最大搜索页数,防止无限循环
+        String adminToken = getAdminToken(imConfig, redisCache);
+        try {
+            while (pageNumber <= maxPages) {
+                JSONObject requestBody = new JSONObject();
+                requestBody.put("userID", recvID);
+                JSONObject pagination = new JSONObject();
+                pagination.put("pageNumber", pageNumber);
+                pagination.put("showNumber", pageSize);
+                requestBody.put("pagination", pagination);
+                String url = IMConfig.URL+"/conversation/get_owner_conversation";
+                String body = HttpRequest.post(url)
+                        .header("operationID", String.valueOf(System.currentTimeMillis()))
+                        .header("token", adminToken)
+                        .body(requestBody.toString())
+                        .execute()
+                        .body();
+                JSONObject jsonResponse = new JSONObject(body);
+                if (jsonResponse.getInt("errCode") == 0) {
+                    JSONObject data = jsonResponse.getJSONObject("data");
+                    int total = data.getInt("total");
+                    if (total > 0) {
+                        JSONArray conversations = data.getJSONArray("conversations");
+                        // 遍历当前页的所有会话
+                        for (int i = 0; i < conversations.length(); i++) {
+                            JSONObject conversation = conversations.getJSONObject(i);
+                            String convId = conversation.getString("conversationID");
+                            // 如果找到目标会话,返回其ex字段
+                            if (conversationId.equals(convId)) {
+                                return conversation.getString("ex");
+                            }
+                        }
+                        // 如果当前页没有找到且已经是最后一页,则退出循环
+                        if (conversations.length() < pageSize) {
+                            break;
+                        }
+                    } else {
+                        // 如果没有数据,直接退出循环
+                        break;
+                    }
+                }
+                // 移动到下一页
+                pageNumber++;
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 修改会话信息
+     *
+     * @param dto 会话修改参数
+     * @param imConfig IM配置
+     * @param redisCache Redis缓存
+     * @return 响应结果
+     */
+    private static OpenImResponseDTO editConversation(OpenImEditConversationDTO dto, IMConfig imConfig, RedisCache redisCache) {
+        String adminToken = getAdminToken(imConfig, redisCache);
+        JSONObject jsonObject = new JSONObject(dto);
+        String url = IMConfig.URL+"/conversation/set_conversations";
+        String body = HttpRequest.post(url)
+                .header("operationID", String.valueOf(System.currentTimeMillis()))
+                .header("token", adminToken)
+                .body(jsonObject.toString())
+                .execute()
+                .body();
+        return JSONUtil.toBean(body, OpenImResponseDTO.class);
+    }
+
+    /**
+     * 获取管理员Token
+     *
+     * @param imConfig IM配置
+     * @param redisCache Redis缓存
+     * @return 管理员Token
+     */
+    private static String getAdminToken(IMConfig imConfig, RedisCache redisCache) {
+        Object cachedTokenObj = redisCache.getCacheObject("openImAdminToken:" + imConfig.getUserID());
+        if (cachedTokenObj != null) {
+            return cachedTokenObj.toString();
+        }
+        JSONObject requestBody = new JSONObject();
+        requestBody.put("secret", imConfig.getSecret());  // 预设的管理员密钥
+        requestBody.put("userID", imConfig.getUserID());   // 管理员 userID
+        String adminToken = null;
+        // 发起 HTTP POST 请求,获取管理员 token
+        try {
+            String url = IMConfig.URL+"/auth/get_admin_token";
+            String response = HttpRequest.post(url)
+                    .header("operationID", String.valueOf(System.currentTimeMillis()))
+                    .body(requestBody.toString())
+                    .execute()
+                    .body();
+
+            JSONObject jsonResponse = new JSONObject(response);
+            if (jsonResponse.getInt("errCode") == 0) {
+                JSONObject data = jsonResponse.getJSONObject("data");
+                adminToken = data.getString("token");
+                redisCache.setCacheObject("openImAdminToken:" + imConfig.getUserID(), adminToken,
+                        data.getInt("expireTimeSeconds"), java.util.concurrent.TimeUnit.SECONDS);
+                return adminToken;
+            }
+        } catch (Exception e) {
+            // 可以记录日志
+            log.error("获取管理员 token 失败", e);
+        }
+        return null;
+    }
+}

+ 4 - 2
fs-service/src/main/resources/application-config-zkzh.yml

@@ -152,10 +152,12 @@ nuonuo:
 ipad:
   url:
   ipadUrl: http://qwipad.muyi88.com
-  aiApi: http://152.136.202.157:3000/api
+  aiApi: http://49.232.181.28:3000/api
   voiceApi:
   commonApi:
 wx_miniapp_temp:
   pay_order_temp_id: VXEvKaGNPFuJmhWK9O_QPrTZxe9umDCukq-maI8Vdek
   inquiry_temp_id: 9POPYeqhI48LOPvq-Rfoklze7H-9SlunJKh10Qt4_2I
-
+tencent:
+  secretId: AKIDdAdvzoQpzuOo11mq2TntjKI5eqrhNp22
+  secretKey: zmLgpzzcwAFNJ4dx2iDTVHgw3ELCrIVg

+ 7 - 3
fs-service/src/main/resources/mapper/fastGpt/FastGptRoleMapper.xml

@@ -20,6 +20,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="reminderWords"    column="reminder_words"    />
         <result property="bindCorpId"    column="bind_corp_id"    />
         <result property="contactInfo"    column="contact_info"    />
+        <result property="userInfo"    column="user_info"    />
         <result property="channelType"    column="channel_type"    />
         <result property="logistics"    column="logistics"    />
         <result property="forbidSendStart"    column="forbid_send_start"    />
@@ -30,9 +31,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectFastGptRoleVo">
-        select role_id, role_name,contact_info,company_id, create_time, update_time, role_type, mode_config_json,
-               mode, kf_id, kf_url, avatar, kf_media_id,reminder_words, bind_corp_id,channel_type,logistics,forbid_send_start,
-               forbid_send_end,forbid_status,send_course_status,course_id
+        select role_id, role_name, contact_info, user_info, company_id, create_time, update_time, role_type, mode_config_json,
+               mode, kf_id, kf_url, avatar, kf_media_id, reminder_words, bind_corp_id, channel_type, logistics, forbid_send_start,
+               forbid_send_end, forbid_status, send_course_status, course_id
         from fastgpt_role
     </sql>
 
@@ -111,6 +112,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="reminderWords != null">reminder_words,</if>
             <if test="bindCorpId != null">bind_corp_id,</if>
             <if test="contactInfo != null">contact_info,</if>
+            <if test="userInfo != null">user_info,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="roleName != null">#{roleName},</if>
@@ -127,6 +129,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="reminderWords != null">#{reminderWords},</if>
             <if test="bindCorpId != null">#{bindCorpId},</if>
             <if test="contactInfo != null">#{contactInfo},</if>
+            <if test="userInfo != null">#{userInfo},</if>
         </trim>
     </insert>
 
@@ -147,6 +150,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="reminderWords != null">reminder_words = #{reminderWords},</if>
             <if test="bindCorpId != null">bind_corp_id = #{bindCorpId},</if>
             <if test="contactInfo != null">contact_info = #{contactInfo},</if>
+            <if test="userInfo != null">user_info = #{userInfo},</if>
             <if test="channelType != null">channel_type = #{channelType},</if>
             <if test="logistics != null">logistics = #{logistics},</if>
             <if test="forbidStatus != null">forbid_status = #{forbidStatus},</if>

+ 136 - 0
fs-service/src/main/resources/mapper/im/ImChatMsgMapper.xml

@@ -0,0 +1,136 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.im.mapper.ImChatMsgMapper">
+
+    <resultMap type="ImChatMsg" id="ImChatMsgResult">
+        <result property="msgId"           column="msg_id"           />
+        <result property="sessionId"       column="session_id"       />
+        <result property="userId"          column="user_id"          />
+        <result property="content"         column="content"          />
+        <result property="msgType"         column="msg_type"         />
+        <result property="companyId"       column="company_id"       />
+        <result property="roleId"          column="role_id"          />
+        <result property="companyUserId"   column="company_user_id"  />
+        <result property="msgJson"         column="msg_json"         />
+        <result property="status"          column="status"           />
+        <result property="nickName"        column="nick_name"        />
+        <result property="avatar"          column="avatar"           />
+        <result property="userType"        column="user_type"        />
+        <result property="shortContent"    column="short_content"    />
+        <result property="sourceFrom"      column="source_from"      />
+        <result property="createTime"      column="create_time"      />
+        <result property="updateTime"      column="update_time"      />
+    </resultMap>
+
+    <sql id="selectImChatMsgVo">
+        select msg_id, session_id, user_id, content, msg_type, company_id, role_id, company_user_id,
+               msg_json, status, nick_name, avatar,  user_type, short_content, source_from,
+               create_time, update_time
+        from im_chat_msg
+    </sql>
+
+    <select id="selectImChatMsgList" parameterType="ImChatMsg" resultMap="ImChatMsgResult">
+        <include refid="selectImChatMsgVo"/>
+        <where>
+            <if test="sessionId != null"> and session_id = #{sessionId}</if>
+            <if test="userId != null and userId != ''"> and user_id = #{userId}</if>
+            <if test="content != null and content != ''"> and content = #{content}</if>
+            <if test="msgType != null"> and msg_type = #{msgType}</if>
+            <if test="companyId != null"> and company_id = #{companyId}</if>
+            <if test="roleId != null"> and role_id = #{roleId}</if>
+            <if test="companyUserId != null"> and company_user_id = #{companyUserId}</if>
+            <if test="status != null"> and status = #{status}</if>
+            <if test="nickName != null and nickName != ''"> and nick_name like concat(#{nickName}, '%')</if>
+            <if test="userType != null"> and user_type = #{userType}</if>
+            <if test="sourceFrom != null"> and source_from = #{sourceFrom}</if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <select id="selectImChatMsgByMsgId" parameterType="Long" resultMap="ImChatMsgResult">
+        <include refid="selectImChatMsgVo"/>
+        where msg_id = #{msgId}
+    </select>
+
+    <select id="getListBySessionId" parameterType="Long" resultMap="ImChatMsgResult">
+        <include refid="selectImChatMsgVo"/>
+        where session_id = #{sessionId}
+        order by create_time asc
+    </select>
+
+    <insert id="insertImChatMsg" parameterType="ImChatMsg" useGeneratedKeys="true" keyProperty="msgId">
+        insert into im_chat_msg
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="sessionId != null">session_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="content != null">content,</if>
+            <if test="msgType != null">msg_type,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="roleId != null">role_id,</if>
+            <if test="companyUserId != null">company_user_id,</if>
+            <if test="msgJson != null">msg_json,</if>
+            <if test="status != null">status,</if>
+            <if test="nickName != null">nick_name,</if>
+            <if test="avatar != null">avatar,</if>
+            <if test="userType != null">user_type,</if>
+            <if test="shortContent != null">short_content,</if>
+            <if test="sourceFrom != null">source_from,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="sessionId != null">#{sessionId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="content != null">#{content},</if>
+            <if test="msgType != null">#{msgType},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="roleId != null">#{roleId},</if>
+            <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="msgJson != null">#{msgJson},</if>
+            <if test="status != null">#{status},</if>
+            <if test="nickName != null">#{nickName},</if>
+            <if test="avatar != null">#{avatar},</if>
+            <if test="userType != null">#{userType},</if>
+            <if test="shortContent != null">#{shortContent},</if>
+            <if test="sourceFrom != null">#{sourceFrom},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+        </trim>
+    </insert>
+
+    <update id="updateImChatMsg" parameterType="ImChatMsg">
+        update im_chat_msg
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="sessionId != null">session_id = #{sessionId},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="content != null">content = #{content},</if>
+            <if test="msgType != null">msg_type = #{msgType},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="roleId != null">role_id = #{roleId},</if>
+            <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
+            <if test="msgJson != null">msg_json = #{msgJson},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="nickName != null">nick_name = #{nickName},</if>
+            <if test="avatar != null">avatar = #{avatar},</if>
+            <if test="userType != null">user_type = #{userType},</if>
+            <if test="shortContent != null">short_content = #{shortContent},</if>
+            <if test="sourceFrom != null">source_from = #{sourceFrom},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where msg_id = #{msgId}
+    </update>
+
+    <delete id="deleteImChatMsgByMsgId" parameterType="Long">
+        delete from im_chat_msg where msg_id = #{msgId}
+    </delete>
+
+    <delete id="deleteImChatMsgByMsgIds" parameterType="String">
+        delete from im_chat_msg where msg_id in
+        <foreach item="msgId" collection="array" open="(" separator="," close=")">
+            #{msgId}
+        </foreach>
+    </delete>
+</mapper>

+ 145 - 0
fs-service/src/main/resources/mapper/im/ImChatSessionMapper.xml

@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.im.mapper.ImChatSessionMapper">
+
+    <resultMap type="ImChatSession" id="ImChatSessionResult">
+        <result property="sessionId"       column="session_id"       />
+        <result property="chatId"          column="chat_id"          />
+        <result property="userId"          column="user_id"          />
+        <result property="kfId"            column="kf_id"            />
+        <result property="companyId"       column="company_id"       />
+        <result property="nickName"        column="nick_name"        />
+        <result property="avatar"          column="avatar"           />
+        <result property="qwUserId"        column="qw_user_id"       />
+        <result property="userName"        column="user_name"        />
+        <result property="personOrAi"      column="person_or_ai"     />
+        <result property="study"           column="study"            />
+        <result property="courseStatus"    column="course_status"    />
+        <result property="courseName"      column="course_name"      />
+        <result property="videoName"       column="video_name"       />
+        <result property="lastTalkTime"    column="last_talk_time"   />
+        <result property="talk"            column="talk"             />
+        <result property="remindStatus"    column="remind_status"    />
+        <result property="remindTime"      column="remind_time"      />
+        <result property="remindCount"     column="remind_count"     />
+        <result property="createTime"      column="create_time"      />
+        <result property="updateTime"      column="update_time"      />
+    </resultMap>
+
+    <sql id="selectImChatSessionVo">
+        select session_id, chat_id, user_id, kf_id, company_id, nick_name, avatar, qw_user_id, user_name,
+               person_or_ai, study, course_status, course_name, video_name, last_talk_time, talk,
+               remind_status, remind_time, remind_count, create_time, update_time
+        from im_chat_session
+    </sql>
+
+    <select id="selectImChatSessionList" parameterType="ImChatSession" resultMap="ImChatSessionResult">
+        <include refid="selectImChatSessionVo"/>
+        <where>
+            <if test="chatId != null and chatId != ''"> and chat_id = #{chatId}</if>
+            <if test="userId != null and userId != ''"> and user_id = #{userId}</if>
+            <if test="kfId != null and kfId != ''"> and kf_id = #{kfId}</if>
+            <if test="companyId != null"> and company_id = #{companyId}</if>
+            <if test="nickName != null and nickName != ''"> and nick_name like concat(#{nickName}, '%')</if>
+            <if test="avatar != null and avatar != ''"> and avatar = #{avatar}</if>
+            <if test="qwUserId != null"> and qw_user_id = #{qwUserId}</if>
+            <if test="userName != null and userName != ''"> and user_name like concat(#{userName}, '%')</if>
+            <if test="personOrAi != null"> and person_or_ai = #{personOrAi}</if>
+            <if test="remindStatus != null"> and remind_status = #{remindStatus}</if>
+        </where>
+        order by create_time desc
+    </select>
+
+    <select id="selectImChatSessionBySessionId" parameterType="Long" resultMap="ImChatSessionResult">
+        <include refid="selectImChatSessionVo"/>
+        where session_id = #{sessionId}
+    </select>
+
+    <insert id="insertImChatSession" parameterType="ImChatSession" useGeneratedKeys="true" keyProperty="sessionId">
+        insert into im_chat_session
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="chatId != null">chat_id,</if>
+            <if test="userId != null">user_id,</if>
+            <if test="kfId != null">kf_id,</if>
+            <if test="companyId != null">company_id,</if>
+            <if test="nickName != null">nick_name,</if>
+            <if test="avatar != null">avatar,</if>
+            <if test="qwUserId != null">qw_user_id,</if>
+            <if test="userName != null">user_name,</if>
+            <if test="personOrAi != null">person_or_ai,</if>
+            <if test="study != null">study,</if>
+            <if test="courseStatus != null">course_status,</if>
+            <if test="courseName != null">course_name,</if>
+            <if test="videoName != null">video_name,</if>
+            <if test="lastTalkTime != null">last_talk_time,</if>
+            <if test="talk != null">talk,</if>
+            <if test="remindStatus != null">remind_status,</if>
+            <if test="remindTime != null">remind_time,</if>
+            <if test="remindCount != null">remind_count,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="chatId != null">#{chatId},</if>
+            <if test="userId != null">#{userId},</if>
+            <if test="kfId != null">#{kfId},</if>
+            <if test="companyId != null">#{companyId},</if>
+            <if test="nickName != null">#{nickName},</if>
+            <if test="avatar != null">#{avatar},</if>
+            <if test="qwUserId != null">#{qwUserId},</if>
+            <if test="userName != null">#{userName},</if>
+            <if test="personOrAi != null">#{personOrAi},</if>
+            <if test="study != null">#{study},</if>
+            <if test="courseStatus != null">#{courseStatus},</if>
+            <if test="courseName != null">#{courseName},</if>
+            <if test="videoName != null">#{videoName},</if>
+            <if test="lastTalkTime != null">#{lastTalkTime},</if>
+            <if test="talk != null">#{talk},</if>
+            <if test="remindStatus != null">#{remindStatus},</if>
+            <if test="remindTime != null">#{remindTime},</if>
+            <if test="remindCount != null">#{remindCount},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+        </trim>
+    </insert>
+
+    <update id="updateImChatSession" parameterType="ImChatSession">
+        update im_chat_session
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="chatId != null">chat_id = #{chatId},</if>
+            <if test="userId != null">user_id = #{userId},</if>
+            <if test="kfId != null">kf_id = #{kfId},</if>
+            <if test="companyId != null">company_id = #{companyId},</if>
+            <if test="nickName != null">nick_name = #{nickName},</if>
+            <if test="avatar != null">avatar = #{avatar},</if>
+            <if test="qwUserId != null">qw_user_id = #{qwUserId},</if>
+            <if test="userName != null">user_name = #{userName},</if>
+            <if test="personOrAi != null">person_or_ai = #{personOrAi},</if>
+            <if test="study != null">study = #{study},</if>
+            <if test="courseStatus != null">course_status = #{courseStatus},</if>
+            <if test="courseName != null">course_name = #{courseName},</if>
+            <if test="videoName != null">video_name = #{videoName},</if>
+            <if test="lastTalkTime != null">last_talk_time = #{lastTalkTime},</if>
+            <if test="talk != null">talk = #{talk},</if>
+            <if test="remindStatus != null">remind_status = #{remindStatus},</if>
+            <if test="remindTime != null">remind_time = #{remindTime},</if>
+            <if test="remindCount != null">remind_count = #{remindCount},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where session_id = #{sessionId}
+    </update>
+
+    <delete id="deleteImChatSessionBySessionId" parameterType="Long">
+        delete from im_chat_session where session_id = #{sessionId}
+    </delete>
+
+    <delete id="deleteImChatSessionBySessionIds" parameterType="String">
+        delete from im_chat_session where session_id in
+        <foreach item="sessionId" collection="array" open="(" separator="," close=")">
+            #{sessionId}
+        </foreach>
+    </delete>
+</mapper>