Pārlūkot izejas kodu

Merge branch 'wx_service'

# Conflicts:
#	netflix-dao/src/main/java/com/cyksj/model/kf/CorpKfUserInfo.java
#	netflix-service/src/main/java/com/cyksj/service/corp/WxCorpOps.java
#	netflix-service/src/main/java/com/cyksj/service/corp/impl/WxCorpOpsImpl.java
zoujiajian 1 gadu atpakaļ
vecāks
revīzija
a9fcfeefb6
67 mainītis faili ar 3505 papildinājumiem un 45 dzēšanām
  1. 13 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountFollowMapper.java
  2. 13 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountMapper.java
  3. 13 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountSessionChatMapper.java
  4. 14 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountSessionConfigMapper.java
  5. 13 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountSyncMsgProgressMapper.java
  6. 13 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountUserChangeRecordMapper.java
  7. 18 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfServicerSessionRecordMapper.java
  8. 13 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfServicerStatisticMapper.java
  9. 13 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfUserMapper.java
  10. 13 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfWaitingQueueUserMapper.java
  11. 21 0
      netflix-dao/src/main/java/com/cyksj/model/dto/CorpKfServicerDto.java
  12. 34 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccount.java
  13. 38 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountFollow.java
  14. 109 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountSessionChat.java
  15. 46 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountSessionConfig.java
  16. 10 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountSessionRecord.java
  17. 21 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountSyncMsgProgress.java
  18. 18 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountUserChangeRecord.java
  19. 61 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfServicerSessionRecord.java
  20. 70 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfServicerStatistic.java
  21. 24 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfUser.java
  22. 18 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfWaitingQueueUser.java
  23. 5 0
      netflix-dao/src/main/java/com/cyksj/model/entity/SysCorpFollow.java
  24. 2 0
      netflix-dao/src/main/java/com/cyksj/model/entity/SysCorpFollowRelate.java
  25. 72 0
      netflix-dao/src/main/java/com/cyksj/model/kf/CorpKfSatisfactionMsg.java
  26. 33 0
      netflix-dao/src/main/java/com/cyksj/model/kf/CorpKfServicerResp.java
  27. 21 0
      netflix-dao/src/main/java/com/cyksj/model/kf/CorpKfSessionResp.java
  28. 1 1
      netflix-dao/src/main/java/com/cyksj/model/kf/CorpKfUserInfo.java
  29. 15 0
      netflix-dao/src/main/java/com/cyksj/model/kf/ServiceStateResponse.java
  30. 38 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpChatMsg.java
  31. 16 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpFileMsg.java
  32. 16 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpImageMsg.java
  33. 87 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpKfMsgSendRequest.java
  34. 26 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpLinkMsg.java
  35. 69 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpMenuMsg.java
  36. 18 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpTextMsg.java
  37. 19 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpVideoMsg.java
  38. 30 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfAccountAdd.java
  39. 30 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfAccountAddResp.java
  40. 25 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfAccountDel.java
  41. 40 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfAccountUpd.java
  42. 42 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfEventMsg.java
  43. 72 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfMsgListResp.java
  44. 25 0
      netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfMsgSendResp.java
  45. 21 0
      netflix-dao/src/main/java/com/cyksj/model/request/CorpKfDutySetReq.java
  46. 12 0
      netflix-dao/src/main/java/com/cyksj/model/request/corp/CorpXmlMessage.java
  47. 31 0
      netflix-dao/src/main/java/com/cyksj/model/request/corp/kf/CorpKfDutyReq.java
  48. 17 0
      netflix-dao/src/main/java/com/cyksj/model/request/sys/SysCorpFollowReq.java
  49. 23 0
      netflix-dao/src/main/java/com/cyksj/model/views/CorpKfDutyServicersStatusView.java
  50. 30 0
      netflix-dao/src/main/java/com/cyksj/model/views/CorpKfTransUserView.java
  51. 89 0
      netflix-dao/src/main/java/com/cyksj/model/views/CorpKfUserChatRecordView.java
  52. 9 0
      netflix-dao/src/main/java/com/cyksj/model/views/SysCorpFollowView.java
  53. 15 0
      netflix-dao/src/main/resources/mapper/CorpKfServicerSessionRecordMapper.xml
  54. 1 0
      netflix-service/src/main/java/com/cyksj/redis/RedisService.java
  55. 75 1
      netflix-service/src/main/java/com/cyksj/service/corp/WxCorpOps.java
  56. 583 3
      netflix-service/src/main/java/com/cyksj/service/corp/impl/CorpServiceImpl.java
  57. 378 39
      netflix-service/src/main/java/com/cyksj/service/corp/impl/WxCorpOpsImpl.java
  58. 36 0
      netflix-service/src/main/java/com/cyksj/service/corp/kf/CorpKfService.java
  59. 380 0
      netflix-service/src/main/java/com/cyksj/service/corp/kf/impl/CorpKfServiceImpl.java
  60. 0 1
      netflix-service/src/main/java/com/cyksj/service/order/impl/OrderCommonServiceImpl.java
  61. 2 0
      netflix-service/src/main/java/com/cyksj/service/sys/SysCorpFollowService.java
  62. 23 0
      netflix-service/src/main/java/com/cyksj/service/sys/impl/SysCorpFollowServiceImpl.java
  63. 50 0
      netflix-web/src/main/java/com/cyksj/web/controller/kf/CorpKfAppletController.java
  64. 12 0
      netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/CmsCorpFollowActiveCodeController.java
  65. 282 0
      netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/wxkf/CorpKfController.java
  66. 86 0
      netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/wxkf/CorpKfMsgController.java
  67. 42 0
      netflix-web/src/main/java/com/cyksj/web/controller/sys/SysCorpFollowController.java

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountFollowMapper.java

@@ -0,0 +1,13 @@
+package com.cyksj.mapper.corp.kf;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CorpKfAccountFollow;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountFollowMapper
+ * 创建者: JavaZou
+ * 创建时间:2024/12/30 18:20
+ */
+public interface CorpKfAccountFollowMapper extends BaseMapper<CorpKfAccountFollow> {
+}

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountMapper.java

@@ -0,0 +1,13 @@
+package com.cyksj.mapper.corp.kf;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CorpKfAccount;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountMapper
+ * 创建者: JavaZou
+ * 创建时间:2024/12/30 18:06
+ */
+public interface CorpKfAccountMapper extends BaseMapper<CorpKfAccount> {
+}

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountSessionChatMapper.java

@@ -0,0 +1,13 @@
+package com.cyksj.mapper.corp.kf;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CorpKfAccountSessionChat;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountSessionChatMapper
+ * 创建者: JavaZou
+ * 创建时间:2025/1/2 13:51
+ */
+public interface CorpKfAccountSessionChatMapper extends BaseMapper<CorpKfAccountSessionChat> {
+}

+ 14 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountSessionConfigMapper.java

@@ -0,0 +1,14 @@
+package com.cyksj.mapper.corp.kf;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CorpKfAccountSessionConfig;
+
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountSessionConfigMapper
+ * 创建者: JavaZou
+ * 创建时间:2024/12/30 18:22
+ */
+public interface CorpKfAccountSessionConfigMapper extends BaseMapper<CorpKfAccountSessionConfig> {
+}

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountSyncMsgProgressMapper.java

@@ -0,0 +1,13 @@
+package com.cyksj.mapper.corp.kf;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CorpKfAccountSyncMsgProgress;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountSyncMsgProgressMapper
+ * 创建者: JavaZou
+ * 创建时间:2025/1/2 11:54
+ */
+public interface CorpKfAccountSyncMsgProgressMapper extends BaseMapper<CorpKfAccountSyncMsgProgress> {
+}

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfAccountUserChangeRecordMapper.java

@@ -0,0 +1,13 @@
+package com.cyksj.mapper.corp.kf;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CorpKfAccountUserChangeRecord;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountUserChangeRecordMapper
+ * 创建者: JavaZou
+ * 创建时间:2025/1/10 10:27
+ */
+public interface CorpKfAccountUserChangeRecordMapper extends BaseMapper<CorpKfAccountUserChangeRecord> {
+}

+ 18 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfServicerSessionRecordMapper.java

@@ -0,0 +1,18 @@
+package com.cyksj.mapper.corp.kf;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.dto.CorpKfServicerDto;
+import com.cyksj.model.entity.CorpKfServicerSessionRecord;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfServicerSessionRecordMapper
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 17:32
+ */
+public interface CorpKfServicerSessionRecordMapper extends BaseMapper<CorpKfServicerSessionRecord> {
+	List<CorpKfServicerDto> getAvailableServicer(@Param("openKfId") String openKfId);
+}

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfServicerStatisticMapper.java

@@ -0,0 +1,13 @@
+package com.cyksj.mapper.corp.kf;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CorpKfServicerStatistic;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfServicerStatisticMapper
+ * 创建者: JavaZou
+ * 创建时间:2025/1/2 16:29
+ */
+public interface CorpKfServicerStatisticMapper extends BaseMapper<CorpKfServicerStatistic> {
+}

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfUserMapper.java

@@ -0,0 +1,13 @@
+package com.cyksj.mapper.corp.kf;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CorpKfUser;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfUserMapper
+ * 创建者: JavaZou
+ * 创建时间:2025/1/2 17:03
+ */
+public interface CorpKfUserMapper extends BaseMapper<CorpKfUser> {
+}

+ 13 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/kf/CorpKfWaitingQueueUserMapper.java

@@ -0,0 +1,13 @@
+package com.cyksj.mapper.corp.kf;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CorpKfWaitingQueueUser;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfWaitingQueueUserMapper
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 16:54
+ */
+public interface CorpKfWaitingQueueUserMapper extends BaseMapper<CorpKfWaitingQueueUser> {
+}

+ 21 - 0
netflix-dao/src/main/java/com/cyksj/model/dto/CorpKfServicerDto.java

@@ -0,0 +1,21 @@
+package com.cyksj.model.dto;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfServicerDto
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 17:43
+ */
+@Getter
+@Setter
+public class CorpKfServicerDto {
+	private String servicerUserid;
+
+	/**
+	 * 接待人数
+	 */
+	private Integer num;
+}

+ 34 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccount.java

@@ -0,0 +1,34 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.ejlchina.searcher.bean.DbField;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccount
+ * 创建者: JavaZou
+ * 创建时间:2024/12/30 18:05
+ */
+@Getter
+@Setter
+public class CorpKfAccount extends BaseEntity{
+	private String name;
+
+	private String img;
+
+	private String openKfId;
+
+	private String url;
+
+	/**
+	 * 使用场景
+	 * 0官网 1其他场景
+	 */
+	private Integer scene;
+
+	@DbField("select 1 from corp_kf_account_session_config cc where cc.open_kf_id = corp_kf_account.open_kf_id")
+	@TableField(exist = false)
+	private Boolean isRefine = false;
+}

+ 38 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountFollow.java

@@ -0,0 +1,38 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountFollow
+ * 创建者: JavaZou
+ * 创建时间:2024/12/30 18:19
+ */
+@Getter
+@Setter
+public class CorpKfAccountFollow extends BaseEntity{
+
+	private String openKfId;
+
+	/**
+	 * 成员id 数组
+	 */
+	private String userIdList;
+
+	/**
+	 * 值班开始时间
+	 */
+	private String startTime;
+
+	/**
+	 * 值班结束时间
+	 */
+	private String endTime;
+
+	/**
+	 * 值班星期
+	 * 1星期天 2星期一 ....
+	 */
+	private Integer dutyWeek;
+}

+ 109 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountSessionChat.java

@@ -0,0 +1,109 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountSessionChat
+ * 创建者: JavaZou
+ * 创建时间:2025/1/2 13:45
+ */
+@Getter
+@Setter
+@SearchBean(tables = "corp_kf_account_session_chat cc" +
+		" LEFT JOIN corp_follow cf on cf.userid = cc.servicer_userid" +
+		" LEFT JOIN corp_kf_user cu on cu.external_userid = cc.external_userid")
+public class CorpKfAccountSessionChat extends BaseEntity{
+	@DbField("cc.open_kf_id")
+	private String openKfId;
+
+	@DbField("cc.session_id")
+	private Long sessionId;
+
+	@DbField("cc.msgid")
+	private String msgid;
+
+	/**
+	 * 消息来源。3-微信客户发送的消息 4-系统推送的事件消息 5-接待人员在企业微信客户端发送的消息
+	 */
+	@DbField("cc.orign")
+	private Integer orign;
+
+	@DbField("cc.servicer_userid")
+	private String servicerUserid;
+
+	/**
+	 * 成员名称
+	 */
+	@DbField("cf.name")
+	@TableField(exist = false)
+	private String followName;
+
+	@DbField("cc.external_userid")
+	private String externalUserid;
+
+	/**
+	 * 用户昵称
+	 */
+	@DbField("cu.nickname")
+	@TableField(exist = false)
+	private String name;
+
+	@DbField("cu.avatar")
+	@TableField(exist = false)
+	private String avatar;
+
+	@DbField("cc.msg_type")
+	private String msgType;
+
+	@DbField("cc.msg_content")
+	private String msgContent;
+
+	/**
+	 * 	链接标题
+	 */
+	@DbField("cc.link_title")
+	private String linkTitle;
+
+	/**
+	 * 链接描述
+	 */
+	@DbField("cc.link_desc")
+	private String linkDesc;
+
+	/**
+	 * 链接 点击后跳转的链接
+	 */
+	@DbField("cc.link_url")
+	private String linkUrl;
+
+	/**
+	 * 链接 缩略图链接
+	 */
+	@DbField("cc.link_pic_url")
+	private String linkPicUrl;
+
+	/**
+	 * 秒长
+	 */
+	@DbField("cc.send_time")
+	private Long sendTime;
+
+	/**
+	 * 发送日期
+	 */
+	@DbField("cc.send_date")
+	private Date sendDate;
+
+	/**
+	 * 图片、视频、文件展示链接
+	 */
+	@DbField("cc.url")
+	private String url;
+}

+ 46 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountSessionConfig.java

@@ -0,0 +1,46 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountSessionConfig
+ * 创建者: JavaZou
+ * 创建时间:2024/12/30 18:20
+ */
+@Getter
+@Setter
+public class CorpKfAccountSessionConfig extends BaseEntity{
+	private String openKfId;
+
+	/**
+	 * 默认0分配接待人员最少值班客服
+	 */
+	private Integer assignType;
+
+	/**
+	 * 接待上限
+	 */
+	private Integer maxNum;
+
+	/**
+	 * 未回复结束对话间隔
+	 */
+	private Integer noReplyTime;
+
+	/**
+	 * 欢迎语
+	 */
+	private String welcomeContent;
+
+	/**
+	 * 排队提示
+	 */
+	private String queueTips;
+
+	/**
+	 * 非值班提示
+	 */
+	private String noDutyTips;
+}

+ 10 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountSessionRecord.java

@@ -0,0 +1,10 @@
+package com.cyksj.model.entity;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountSessionRecord
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 15:14
+ */
+public class CorpKfAccountSessionRecord {
+}

+ 21 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountSyncMsgProgress.java

@@ -0,0 +1,21 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountSyncMsgProgress
+ * 创建者: JavaZou
+ * 创建时间:2025/1/2 11:53
+ */
+@Getter
+@Setter
+public class CorpKfAccountSyncMsgProgress extends BaseEntity{
+	private String openKfId;
+
+	/**
+	 * 上一次调用时返回的next_cursor
+	 */
+	private String nextCursor;
+}

+ 18 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfAccountUserChangeRecord.java

@@ -0,0 +1,18 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfAccountUserChangeRecord
+ * 创建者: JavaZou
+ * 创建时间:2025/1/10 10:26
+ */
+@Getter
+@Setter
+public class CorpKfAccountUserChangeRecord extends BaseEntity{
+	private Long cmsUserId;
+
+	private String openKfId;
+}

+ 61 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfServicerSessionRecord.java

@@ -0,0 +1,61 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfServicerSessionRecord
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 17:31
+ */
+@Getter
+@Setter
+public class CorpKfServicerSessionRecord extends BaseEntity{
+	private String openKfId;
+
+	/**
+	 * 会话转接客服id
+	 */
+	private String fromUserid;
+
+	private String servicerUserid;
+
+	private String externalUserid;
+
+	private String msgid;
+
+	/**
+	 * 接待状态
+	 * 0接待中 1已结束,2转接出去
+	 */
+	private Integer serviceState;
+
+	/**
+	 * 变更类型,均为接待人员在企业微信客户端操作触发。1-从接待池接入会话 2-转接会话 3-结束会话 4-重新接入已结束/已转接会话
+	 */
+	private Integer changeType;
+
+	/**
+	 * 0未评价 1.满意 2.一般 3.不满意
+	 * 满意度
+	 */
+	private Integer satisfaction;
+
+	/**
+	 * 是否发送满意度测评 默认false
+	 */
+	private Boolean sendSatisfaction;
+
+	/**
+	 * 会话开始时间
+	 */
+	private Date startTime;
+
+	/**
+	 * 会话关闭时间
+	 */
+	private Date closeTime;
+}

+ 70 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfServicerStatistic.java

@@ -0,0 +1,70 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.ejlchina.searcher.bean.DbIgnore;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Getter
+@Setter
+public class CorpKfServicerStatistic extends BaseEntity {
+
+	/** openkfid */
+	private String openKfId;
+
+	/** 客服人员 ID */
+	private String servicerUserid;
+
+	/**
+	 * 账号
+	 */
+	@DbIgnore
+	@TableField(exist = false)
+	private String name;
+
+	/**
+	 * 企业成员名
+	 */
+	@DbIgnore
+	@TableField(exist = false)
+	private String followName;
+
+	/** 接入人工会话数 */
+	private Integer sessionCnt;
+
+	/** 咨询客户数 */
+	private Integer customerCnt;
+
+	/** 咨询消息总数 */
+	private Integer customerMsgCnt;
+
+	/** 人工回复率 */
+	private BigDecimal replyRate;
+
+	/** 平均首次响应时长(秒) */
+	private BigDecimal firstReplyAverageSec;
+
+	/** 满意度评价发送数 */
+	private Integer satisfactionInvestgateCnt;
+
+	/** 满意度参评率 */
+	private BigDecimal satisfactionParticipationRate;
+
+	/** 满意评价占比 */
+	private BigDecimal satisfiedRate;
+
+	/** 一般评价占比 */
+	private BigDecimal middlingRate;
+
+	/** 不满意评价占比 */
+	private BigDecimal dissatisfiedRate;
+
+	/** 被拒收消息的客户数 */
+	private Integer msgRejectedCustomerCnt;
+
+	/** 统计日期 */
+	private Date statisticsDate;
+}

+ 24 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfUser.java

@@ -0,0 +1,24 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfUser
+ * 创建者: JavaZou
+ * 创建时间:2025/1/2 17:02
+ */
+@Getter
+@Setter
+public class CorpKfUser extends BaseEntity{
+	private String externalUserid;
+
+	private String unionid;
+
+	private String nickname;
+
+	private Integer gender;
+
+	private String avatar;
+}

+ 18 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpKfWaitingQueueUser.java

@@ -0,0 +1,18 @@
+package com.cyksj.model.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfWaitingQueueUser
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 16:53
+ */
+@Getter
+@Setter
+public class CorpKfWaitingQueueUser extends BaseEntity{
+	private String openKfId;
+
+	private String externalUserid;
+}

+ 5 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/SysCorpFollow.java

@@ -15,4 +15,9 @@ public class SysCorpFollow extends BaseEntity{
 	private String name;
 
 	private Boolean deleted;
+
+	/**
+	 * 登录账号用户id
+	 */
+	private Long cmsUserId;
 }

+ 2 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/SysCorpFollowRelate.java

@@ -25,5 +25,7 @@ public class SysCorpFollowRelate extends BaseEntity{
 
 	private String img;
 
+	private String toFollowId;
+
 	private Boolean deleted;
 }

+ 72 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/CorpKfSatisfactionMsg.java

@@ -0,0 +1,72 @@
+package com.cyksj.model.kf;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfSatisfactionMsg
+ * 创建者: JavaZou
+ * 创建时间:2025/1/2 14:07
+ */
+@Getter
+@Setter
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class CorpKfSatisfactionMsg {
+	private String touser;
+	private String open_kfid;
+	private String msgid;
+	private String msgtype;
+	private MsgMenu msgmenu;
+	private String code;
+
+	@Getter
+	@Setter
+	public static class MsgMenu {
+		private String head_content;
+		private List<MenuItem> list;
+		private String tail_content;
+
+		@Getter
+		@Setter
+		public static class MenuItem {
+			private String type;
+			private Click click;
+//			private View view;
+//			private MiniProgram miniprogram;
+//			private Text text;
+
+			@Getter
+			@Setter
+			public static class Click {
+				private String id;
+				private String content;
+			}
+
+			@Getter
+			@Setter
+			public static class View {
+				private String url;
+				private String content;
+			}
+
+			@Getter
+			@Setter
+			public static class MiniProgram {
+				private String appid;
+				private String pagepath;
+				private String content;
+			}
+
+			@Getter
+			@Setter
+			public static class Text {
+				private String content;
+				private int no_newline;
+			}
+		}
+	}
+}

+ 33 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/CorpKfServicerResp.java

@@ -0,0 +1,33 @@
+package com.cyksj.model.kf;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfServicerList
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 16:32
+ */
+@Getter
+@Setter
+public class CorpKfServicerResp {
+	private Long errcode;
+
+	private String errmsg;
+
+	@JsonProperty("servicer_list")
+	private List<CorpKfServicer> servicerList;
+
+	@Getter
+	@Setter
+	public static class CorpKfServicer{
+		private String userid;
+
+		//接待人员的接待状态。0:接待中,1:停止接待。
+		private Integer status;
+	}
+}

+ 21 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/CorpKfSessionResp.java

@@ -0,0 +1,21 @@
+package com.cyksj.model.kf;
+
+import com.cyksj.model.entity.BaseEntity;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfSessionResp
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 17:00
+ */
+@Getter
+@Setter
+public class CorpKfSessionResp extends BaseEntity {
+	private Long errcode;
+
+	private String errmsg;
+
+	private String msg_code;
+}

+ 1 - 1
netflix-dao/src/main/java/com/cyksj/model/kf/CorpKfUserInfo.java

@@ -23,4 +23,4 @@ public class CorpKfUserInfo {
 	private String unionid;
 
 	private Integer gender;
-}
+}

+ 15 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/ServiceStateResponse.java

@@ -0,0 +1,15 @@
+package com.cyksj.model.kf;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class ServiceStateResponse {
+    private int errcode;
+    private String errmsg;
+    //0未处理 1由智能助手接待 2待接入池排队中 3由人工接待 4已结束/未开始
+    private Integer service_state;
+    //接待人员的userid。第三方应用为密文userid,即open_userid。仅当state=3时有效
+    private String servicer_userid;
+}

+ 38 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpChatMsg.java

@@ -0,0 +1,38 @@
+package com.cyksj.model.kf;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: WxCorpChatMsg
+ * 创建者: JavaZou
+ * 创建时间:2024/12/31 12:05
+ */
+@Getter
+@Setter
+public class WxCorpChatMsg {
+	private String title;
+
+	private String msgid;
+
+	private String external_userid;
+
+	private String servicer_userid;
+
+	//消息来源。3-微信客户发送的消息 4-系统推送的事件消息 5-接待人员在企业微信客户端发送的消息
+	private Integer origin;
+
+	private List<Item> item;
+
+	@Getter
+	@Setter
+	public static class Item{
+		private Long send_time;
+		private String msgtype;
+		private String sender_name;
+		private String msg_content;
+	}
+}

+ 16 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpFileMsg.java

@@ -0,0 +1,16 @@
+package com.cyksj.model.kf;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: WxCorpFileMsg
+ * 创建者: JavaZou
+ * 创建时间:2025/1/6 17:40
+ */
+@Getter
+@Setter
+public class WxCorpFileMsg {
+	private String media_id;
+}

+ 16 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpImageMsg.java

@@ -0,0 +1,16 @@
+package com.cyksj.model.kf;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: WxCorpImageMsg
+ * 创建者: JavaZou
+ * 创建时间:2025/1/6 17:29
+ */
+@Getter
+@Setter
+public class WxCorpImageMsg {
+	private String media_id;
+}

+ 87 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpKfMsgSendRequest.java

@@ -0,0 +1,87 @@
+package com.cyksj.model.kf;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author chan
+ * @date 2023-05-12 12:17 PM
+ */
+@NoArgsConstructor
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class WxCorpKfMsgSendRequest {
+  /**
+   * (发送欢迎语等事件响应消息) 事件响应消息对应的code。通过事件回调下发,仅可使用一次。
+   */
+  private String code;
+  /**
+   * <pre>
+   *   参数:touser
+   *   是否必须:是
+   *   类型:string
+   *   说明:指定接收消息的客户UserID
+   * </pre>
+   */
+  @JsonProperty("touser")
+  private String toUser;
+  /**
+   * <pre>
+   *   参数:open_kfid
+   *   是否必须:是
+   *   类型:string
+   *   说明:指定发送消息的客服帐号ID
+   * </pre>
+   */
+  @JsonProperty("open_kfid")
+  private String openKfid;
+  /**
+   * <pre>
+   *   参数:msgid
+   *   是否必须:否
+   *   类型:string
+   *   说明:指定消息ID
+   * </pre>
+   */
+  @JsonProperty("msgid")
+  private String msgId;
+  /**
+   * <pre>
+   *   参数:msgtype
+   *   是否必须:是
+   *   类型:string
+   *   说明:消息类型,(text,image,voice,video,file,link,miniprogram,msgmenu,location)
+   * </pre>
+   */
+  @JsonProperty("msgtype")
+  private String msgType;
+  /**
+   * <pre>
+   *   参数:text
+   *   是否必须:是
+   *   类型:obj
+   *   说明:文本消息
+   * </pre>
+   */
+  private WxCpKfTextMsg text;
+
+
+  @Data
+  public static class WxCpKfTextMsg {
+
+    /**
+     * <pre>
+     *   参数:content
+     *   是否必须:是
+     *   类型:string
+     *   说明:消息内容,最长不超过2048个字节
+     * </pre>
+     */
+    private String content;
+    @JsonProperty("menu_id")
+    private String menuId;
+  }
+
+}

+ 26 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpLinkMsg.java

@@ -0,0 +1,26 @@
+package com.cyksj.model.kf;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: WxCorpLinkMsg
+ * 创建者: JavaZou
+ * 创建时间:2025/1/6 17:39
+ */
+@Getter
+@Setter
+public class WxCorpLinkMsg {
+	//链接消息
+	private String title;
+
+	//描述
+	private String desc;
+
+	//点击后跳转的链接
+	private String url;
+
+	//缩略图链接
+	private String pic_url;
+}

+ 69 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpMenuMsg.java

@@ -0,0 +1,69 @@
+package com.cyksj.model.kf;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: WxCorpMenuMsg
+ * 创建者: JavaZou
+ * 创建时间:2025/1/6 17:41
+ */
+@Getter
+@Setter
+public class WxCorpMenuMsg {
+	//	起始文本
+	private String head_content;
+
+	/**
+	 * 菜单项配置
+	 */
+	private List<ListData> list;
+
+	//结束文本
+	private String tail_content;
+
+	@Getter
+	@Setter
+	public static class ListData{
+		//类型
+		private String type;
+
+		//点击
+		private Click click;
+
+		//跳转页面
+		private View view;
+
+		//小程序
+		private Miniprogram miniprogram;
+
+		@Getter
+		@Setter
+		public static class Click {
+			private String id;
+
+			private String content;
+		}
+
+		@Getter
+		@Setter
+		public static class View {
+			private String url;
+
+			private String content;
+		}
+
+		@Getter
+		@Setter
+		public static class Miniprogram {
+			private String appid;
+
+			private String pagepath;
+
+			private String content;
+		}
+	}
+}

+ 18 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpTextMsg.java

@@ -0,0 +1,18 @@
+package com.cyksj.model.kf;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: WxCorpTextMsg
+ * 创建者: JavaZou
+ * 创建时间:2025/1/2 15:41
+ */
+@Getter
+@Setter
+public class WxCorpTextMsg {
+	private String content;
+
+	private String menu_id;
+}

+ 19 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCorpVideoMsg.java

@@ -0,0 +1,19 @@
+package com.cyksj.model.kf;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: WxCorpVideoMsg
+ * 创建者: JavaZou
+ * 创建时间:2025/1/6 17:28
+ */
+@Getter
+@Setter
+public class WxCorpVideoMsg {
+	/**
+	 * 图片文件id
+	 */
+	private String media_id;
+}

+ 30 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfAccountAdd.java

@@ -0,0 +1,30 @@
+package com.cyksj.model.kf;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 添加客服帐号-请求参数
+ */
+@NoArgsConstructor
+@Data
+public class WxCpKfAccountAdd implements Serializable {
+
+	private static final long serialVersionUID = 3565729481246537411L;
+
+	/**
+	 * 客服名称;不多于16个字符
+	 */
+	@JsonProperty("name")
+	private String name;
+
+	/**
+	 * 客服头像临时素材。可以调用上传临时素材接口获取。
+	 * 不多于128个字节
+	 */
+	@JsonProperty("media_id")
+	private String mediaId;
+}

+ 30 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfAccountAddResp.java

@@ -0,0 +1,30 @@
+package com.cyksj.model.kf;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 添加客服帐号-返回结果
+ *
+ */
+@NoArgsConstructor
+@Data
+public class WxCpKfAccountAddResp {
+
+
+  @JsonProperty("errcode")
+  protected Long errcode;
+
+  @JsonProperty("errmsg")
+  protected String errmsg;
+
+  private static final long serialVersionUID = -6649323005421772827L;
+
+  /**
+   * 新创建的客服帐号ID
+   */
+  @JsonProperty("open_kfid")
+  private String openKfid;
+
+}

+ 25 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfAccountDel.java

@@ -0,0 +1,25 @@
+package com.cyksj.model.kf;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 删除客服帐号-请求参数
+ */
+@NoArgsConstructor
+@Data
+public class WxCpKfAccountDel implements Serializable {
+
+  private static final long serialVersionUID = 1997221467585676772L;
+
+  /**
+   * 客服帐号ID。
+   * 不多于64字节
+   */
+  @JsonProperty("open_kfid")
+  private String openKfid;
+
+}

+ 40 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfAccountUpd.java

@@ -0,0 +1,40 @@
+package com.cyksj.model.kf;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 修改客服帐号-请求参数
+ *
+ * @author Fu  created on  2022/1/19 19:10
+ */
+@NoArgsConstructor
+@Data
+public class WxCpKfAccountUpd implements Serializable {
+
+  private static final long serialVersionUID = -900712046553752529L;
+
+  /**
+   * 要修改的客服帐号ID。
+   * 不多于64字节
+   */
+  @JsonProperty("open_kfid")
+  private String openKfid;
+
+  /**
+   * 新的客服名称,如不需要修改可不填。
+   * 不多于16个字符
+   */
+  @JsonProperty("name")
+  private String name;
+
+  /**
+   * 新的客服头像临时素材,如不需要修改可不填。可以调用上传临时素材接口获取。
+   * 不多于128个字节
+   */
+  @JsonProperty("media_id")
+  private String mediaId;
+}

+ 42 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfEventMsg.java

@@ -0,0 +1,42 @@
+package com.cyksj.model.kf;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @author chan
+ * @date 2023-05-12 11:27 AM
+ */
+@NoArgsConstructor
+@Data
+public class WxCpKfEventMsg {
+  @JsonProperty("event_type")
+  private String eventType;
+  @JsonProperty("open_kfid")
+  private String openKfid;
+  @JsonProperty("external_userid")
+  private String externalUserId;
+  @JsonProperty("servicer_userid")
+  private String servicerUserId;
+  @JsonProperty("old_servicer_userid")
+  private String oldServicerUserId;
+  @JsonProperty("new_servicer_userid")
+  private String newServicerUserId;
+  private String scene;
+  @JsonProperty("scene_param")
+  private String sceneParam;
+  @JsonProperty("welcome_code")
+  private String welcomeCode;
+  @JsonProperty("fail_msgid")
+  private String failMsgId;
+  @JsonProperty("fail_type")
+  private Integer failType;
+  private Integer status;
+  @JsonProperty("change_type")
+  private Integer changeType;
+  @JsonProperty("msg_code")
+  private String msgCode;
+  @JsonProperty("recall_msgid")
+  private String recallMsgId;
+}

+ 72 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfMsgListResp.java

@@ -0,0 +1,72 @@
+package com.cyksj.model.kf;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+
+@NoArgsConstructor
+@Data
+public class WxCpKfMsgListResp {
+
+
+  @JsonProperty("errcode")
+  protected Long errcode;
+
+  @JsonProperty("errmsg")
+  protected String errmsg;
+
+  private static final long serialVersionUID = -3115552079069452091L;
+  @JsonProperty("next_cursor")
+  private String nextCursor;
+
+  /**
+   * 是否还有更多数据。0-否;1-是。
+   */
+  @JsonProperty("has_more")
+  private Integer hasMore;
+
+  @JsonProperty("msg_list")
+  private List<WxCpKfMsgItem> msgList;
+
+  @NoArgsConstructor
+  @Data
+  public static class WxCpKfMsgItem {
+    @JsonProperty("msgid")
+    private String msgId;
+    @JsonProperty("open_kfid")
+    private String openKfid;
+    @JsonProperty("external_userid")
+    private String externalUserId;
+    @JsonProperty("send_time")
+    private Long sendTime;
+    //消息来源。3-微信客户发送的消息 4-系统推送的事件消息 5-接待人员在企业微信客户端发送的消息
+    private Integer origin;
+    @JsonProperty("servicer_userid")
+    private String servicerUserId;
+    @JsonProperty("old_servicer_userid")
+    private String oldServicerUserid;
+    @JsonProperty("new_servicer_userid")
+    private String newServicerUserid;
+    @JsonProperty("msgtype")
+    private String msgType;
+    private WxCpKfEventMsg event;
+    //聊天记录事件
+    @JsonProperty("merged_msg")
+    private WxCorpChatMsg mergedMsg;
+    //文本消息
+    private WxCorpTextMsg text;
+    //图片消息
+    private WxCorpImageMsg image;
+    //视频消息
+    private WxCorpVideoMsg video;
+    //链接消息
+    private WxCorpLinkMsg link;
+    //文件消息
+    private WxCorpFileMsg file;
+    //菜单消息
+    private WxCorpMenuMsg msgmenu;
+  }
+}

+ 25 - 0
netflix-dao/src/main/java/com/cyksj/model/kf/WxCpKfMsgSendResp.java

@@ -0,0 +1,25 @@
+package com.cyksj.model.kf;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * The type Wx cp kf msg send resp.
+ *
+ */
+@NoArgsConstructor
+@Data
+public class WxCpKfMsgSendResp{
+
+
+  @JsonProperty("errcode")
+  protected Long errcode;
+
+  @JsonProperty("errmsg")
+  protected String errmsg;
+
+  @JsonProperty("msgid")
+  private String msgId;
+  
+}

+ 21 - 0
netflix-dao/src/main/java/com/cyksj/model/request/CorpKfDutySetReq.java

@@ -0,0 +1,21 @@
+package com.cyksj.model.request;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfDutySetReq
+ * 创建者: JavaZou
+ * 创建时间:2025/1/3 17:44
+ */
+@Getter
+@Setter
+public class CorpKfDutySetReq {
+	private String openKfId;
+
+	/**
+	 * 1上班 1下班
+	 */
+	private Boolean status;
+}

+ 12 - 0
netflix-dao/src/main/java/com/cyksj/model/request/corp/CorpXmlMessage.java

@@ -242,6 +242,18 @@ public class CorpXmlMessage implements Serializable {
 	@JacksonXmlProperty(localName = "LinkId")
 	private String LinkId;
 
+	// kf
+	/**
+	 * 调用拉取消息接口时,需要传此token,用于校验请求的合法性
+	 */
+	@JacksonXmlProperty(localName = "Token")
+	private String token;
+
+	/**
+	 * 有新消息的客服帐号。可通过sync_msg接口指定open_kfid获取此客服帐号的消息
+	 */
+	@JacksonXmlProperty(localName = "OpenKfId")
+	private String openKfId;
 
 	public static CorpXmlMessage fromXml(String xml) {
 		//修改微信变态的消息内容格式,方便解析

+ 31 - 0
netflix-dao/src/main/java/com/cyksj/model/request/corp/kf/CorpKfDutyReq.java

@@ -0,0 +1,31 @@
+package com.cyksj.model.request.corp.kf;
+
+import com.cyksj.model.entity.CorpKfAccountFollow;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfDutyReq
+ * 创建者: JavaZou
+ * 创建时间:2025/1/2 17:09
+ */
+@Getter
+@Setter
+public class CorpKfDutyReq {
+	private String openKfId;
+
+	private List<CorpKfAccountFollow> dutyList;
+
+	/**
+	 * 是否修改单个
+	 */
+	private Boolean isUpdateSingle = false;
+
+	/**
+	 * 删除ids
+	 */
+	private List<Long> delIds;
+}

+ 17 - 0
netflix-dao/src/main/java/com/cyksj/model/request/sys/SysCorpFollowReq.java

@@ -3,6 +3,8 @@ package com.cyksj.model.request.sys;
 import lombok.Getter;
 import lombok.Setter;
 
+import java.util.List;
+
 /**
  * 项目名: yhlxj
  * 文件名: SysCorpFollowReq
@@ -34,6 +36,21 @@ public class SysCorpFollowReq {
 	 */
 	private String followId;
 
+	/**
+	 * 转接客服
+	 */
+	private String toFollowId;
+
+	/**
+	 * 转接数量
+	 */
+	private Integer transNum;
+
+	/**
+	 * 转接用户列表
+	 */
+	private List<String> transUserIds;
+
 	private Integer status;
 
 	private String img;

+ 23 - 0
netflix-dao/src/main/java/com/cyksj/model/views/CorpKfDutyServicersStatusView.java

@@ -0,0 +1,23 @@
+package com.cyksj.model.views;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfDutyServicers
+ * 创建者: JavaZou
+ * 创建时间:2025/1/3 17:30
+ */
+@Getter
+@Setter
+public class CorpKfDutyServicersStatusView {
+	private String name;
+
+	private String followName;
+
+	private Long cmsUserId;
+
+	//0未值班
+	private Integer status;
+}

+ 30 - 0
netflix-dao/src/main/java/com/cyksj/model/views/CorpKfTransUserView.java

@@ -0,0 +1,30 @@
+package com.cyksj.model.views;
+
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfTransUserView
+ * 创建者: JavaZou
+ * 创建时间:2025/1/8 18:15
+ */
+@Getter
+@Setter
+@SearchBean(tables = "corp_kf_servicer_session_record cf left join corp_kf_user cu on cu.external_userid = cf.external_userid",
+where = "cf.service_state = 0")
+public class CorpKfTransUserView {
+	@DbField("cf.external_userid")
+	private String externalUserid;
+
+	@DbField("cu.name")
+	private String name;
+
+	@DbField("cf.open_kf_id")
+	private String openKfId;
+
+	@DbField("cf.servicer_userid")
+	private String servicerUserid;
+}

+ 89 - 0
netflix-dao/src/main/java/com/cyksj/model/views/CorpKfUserChatRecordView.java

@@ -0,0 +1,89 @@
+package com.cyksj.model.views;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.Date;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfUserChatView
+ * 创建者: JavaZou
+ * 创建时间:2025/1/10 11:30
+ */
+@Getter
+@Setter
+@SearchBean(tables = "corp_kf_servicer_session_record cr LEFT JOIN corp_follow cf on cf.userid = cr.servicer_userid" +
+		" LEFT JOIN corp_kf_user cu on cu.external_userid = cr.external_userid")
+public class CorpKfUserChatRecordView {
+	@DbField("cr.id")
+	private Long sessionId;
+
+	@DbField("cr.open_kf_id")
+	private String openKfId;
+
+	/**
+	 * 会话转接客服id
+	 */
+	@DbField("cr.from_userid")
+	private String fromUserid;
+
+	@DbField("cr.servicer_userid")
+	private String servicerUserid;
+
+	/**
+	 * 成员名称
+	 */
+	@DbField("cf.name")
+	@TableField(exist = false)
+	private String followName;
+
+	@DbField("cr.external_userid")
+	private String externalUserid;
+
+	/**
+	 * 用户昵称
+	 */
+	@DbField("cu.nickname")
+	@TableField(exist = false)
+	private String name;
+
+	@DbField("cu.avatar")
+	@TableField(exist = false)
+	private String avatar;
+
+	/**
+	 * 接待状态
+	 * 0接待中 1已结束,2转接出去
+	 */
+	@DbField("cr.service_state")
+	private Integer serviceState;
+
+	/**
+	 * 0未评价 1.满意 2.一般 3.不满意
+	 * 满意度
+	 */
+	@DbField("cr.satisfaction")
+	private Integer satisfaction;
+
+	/**
+	 * 是否发送满意度测评 默认false
+	 */
+	@DbField("cr.send_satisfaction")
+	private Boolean sendSatisfaction;
+
+	/**
+	 * 会话开始时间
+	 */
+	@DbField("cr.start_time")
+	private Date startTime;
+
+	/**
+	 * 会话关闭时间
+	 */
+	@DbField("cr.close_time")
+	private Date closeTime;
+}

+ 9 - 0
netflix-dao/src/main/java/com/cyksj/model/views/SysCorpFollowView.java

@@ -60,6 +60,15 @@ public class SysCorpFollowView {
 	@DbIgnore
 	private Boolean isOpen = false;
 
+	/**
+	 * 登录账号用户id
+	 */
+	@DbField("sf.cms_user_id")
+	private Long cmsUserId;
+
+	@DbField("select nickname from cms_user where id = cms_user_id limit 1")
+	private String cmsUserName;
+
 	public String getShowName() {
 		StringBuilder sb = new StringBuilder();
 		sb.append(name).append("(").append(followName).append(")");

+ 15 - 0
netflix-dao/src/main/resources/mapper/CorpKfServicerSessionRecordMapper.xml

@@ -0,0 +1,15 @@
+<?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.cyksj.mapper.corp.kf.CorpKfServicerSessionRecordMapper">
+
+
+    <select id="getAvailableServicer" resultType="com.cyksj.model.dto.CorpKfServicerDto">
+        select servicer_userid,count(1) as num
+        from corp_kf_servicer_session_record
+        where open_kf_id = #{openKfId}
+          and service_state = 0
+        group by servicer_userid
+        order by num asc
+    </select>
+</mapper>

+ 1 - 0
netflix-service/src/main/java/com/cyksj/redis/RedisService.java

@@ -853,6 +853,7 @@ public class RedisService {
         USER_MIRROR_SHOP_AUTH_SUCCESS_KEY("user_mirror_shop_auth_success_key:%s", "镜像店铺主后台微信授权", 60 * 10),
         USER_MIRROR_SHOP_ADVERTISE_CACHE_KEY("user_mirror_shop_advertise_cache_key:", "用户镜像店铺广告缓存key", 60 * 60 * 12),
         NETFLIX_CHANGE_TICKET_KEY("netflix_change_ticket_key:", "奈飞更换车票key", 60l),
+        CORP_KF_ACCOUNT_URL("corp_kf_account_url:", "微信客服url:", 60 * 60 * 6),
         ;
 
         private String name;

+ 75 - 1
netflix-service/src/main/java/com/cyksj/service/corp/WxCorpOps.java

@@ -2,7 +2,7 @@ package com.cyksj.service.corp;
 
 import cn.hutool.json.JSONObject;
 import com.cyksj.dto.CorpOauth2UserInfo;
-import com.cyksj.model.kf.CorpKfUserInfo;
+import com.cyksj.model.kf.*;
 import com.cyksj.model.request.corp.CorpFollowContact;
 import com.cyksj.model.request.corp.CorpGroupConfig;
 import com.cyksj.model.request.corp.CorpMsgTemplate;
@@ -14,6 +14,7 @@ import com.cyksj.model.request.corp.message.WxCorpAgentMessage;
 import com.cyksj.model.response.corp.*;
 
 import java.util.List;
+import java.util.Set;
 
 /*
  *项目名: netflix
@@ -190,5 +191,78 @@ public interface WxCorpOps {
 	 */
 	List<String> getPermitUserList(String corpId, String type) throws Exception;
 
+	/**
+	 * 添加客服帐号,并可设置客服名称和头像
+	 */
+	WxCpKfAccountAddResp addAccount(String corpId, WxCpKfAccountAdd add) throws Exception;
+
+	/**
+	 * 删除已有的客服帐号
+	 *
+	 * @param del 要删除的客服帐号
+	 */
+	void delAccount(String corpId, WxCpKfAccountDel del) throws Exception;
+
+	/**
+	 * 修改已有的客服帐号,可修改客服名称和头像。
+	 *
+	 * @param upd 新的客服账号信息
+	 */
+	void updAccount(String corpId, WxCpKfAccountUpd upd) throws Exception;
+
+	/**
+	 * 读取消息
+	 */
+	WxCpKfMsgListResp syncMsg(String corpId, String cursor, String token, Integer limit, String openKfId) throws Exception;
+
+	/**
+	 * 客服发送消息
+	 */
+	WxCpKfMsgSendResp sendMsgOnEvent(String corpId, WxCorpKfMsgSendRequest request) throws Exception;
+
+	/**
+	 * 获取客服账号接待人员
+	 */
+	CorpKfServicerResp getKfServicerList(String corpId,String openKfId) throws Exception;
+
+	void moveWaitingQueue(String corpId, String openKfId, String externalUserId) throws Exception;
+
+	/**
+	 * 变更微信客服会话状态
+	 */
+	void transCorpKfSessionToServicer(String toUserName, String openKfId, String userid, String externalUserId, Integer serviceState) throws Exception;
+
+	/**
+	 * 发送满意度调查msg
+	 */
+	void sendSatisfactionMsg(String msgid, String openKfId, String externalUserId, String msgCode) throws Exception;
+
 	CorpKfUserInfo getCorpKfUserInfo(String externalUserid) throws Exception;
+
+	/**
+	 * 获取微信客服账号链接
+	 */
+	String getCorpKfUrl(String openKfId, String scene) throws Exception;
+
+	/**
+	 * 删除接待人员
+	 */
+	void delCorpKfServicer(String openKfId, List<String> delDutyFollowId) throws Exception;
+
+	/**
+	 * 添加接待人员
+	 */
+	void addCorpKfServicer(String openKfId, Set<String> dutyFollowIds) throws Exception;
+
+	/**
+	 * 发送文本消息
+	 */
+	void sendTextMsg(String corpId, WxCorpKfMsgSendRequest wxCpKfMsgSendRequest) throws Exception;
+
+	ServiceStateResponse getCorpKfSessionStatus(String corpId, String openKfId, String externalUserid) throws Exception;
+
+	/**
+	 * 获取素材
+	 */
+	String getMedia(String corpId, String mediaId) throws Exception;
 }

+ 583 - 3
netflix-service/src/main/java/com/cyksj/service/corp/impl/CorpServiceImpl.java

@@ -2,7 +2,10 @@ package com.cyksj.service.corp.impl;
 
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.RandomUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HttpUtil;
 import cn.hutool.json.JSONUtil;
@@ -10,6 +13,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.cyksj.common.constant.Constant;
 import com.cyksj.common.exception.BusinessRuntimeException;
 import com.cyksj.common.task.GlobalThreadPoolTaskExecutor;
+import com.cyksj.common.util.Jsons;
 import com.cyksj.common.util.StringUtil;
 import com.cyksj.common.util.Xmls;
 import com.cyksj.common.util.corp.WxCryptUtil;
@@ -18,8 +22,11 @@ import com.cyksj.config.corp.YHLXWxCorpConfig;
 import com.cyksj.dto.CorpOauth2UserInfo;
 import com.cyksj.mapper.OrderDonMapper;
 import com.cyksj.mapper.corp.*;
+import com.cyksj.mapper.corp.kf.*;
+import com.cyksj.model.dto.CorpKfServicerDto;
 import com.cyksj.model.dto.WxCorpEncryptDto;
 import com.cyksj.model.entity.*;
+import com.cyksj.model.kf.*;
 import com.cyksj.model.request.corp.Attachments;
 import com.cyksj.model.request.corp.CorpXmlMessage;
 import com.cyksj.model.request.corp.CorpXmlOutMessage;
@@ -42,10 +49,9 @@ import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.PostConstruct;
 import java.io.File;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
 
 /*
  *项目名: netflix
@@ -97,6 +103,30 @@ public class CorpServiceImpl implements CorpService {
 	@Autowired
 	private CorpUserFollowRelationMapper corpUserFollowRelationMapper;
 
+	@Autowired
+	private CorpKfAccountMapper corpKfAccountMapper;
+
+	@Autowired
+	private CorpKfAccountFollowMapper corpKfAccountFollowMapper;
+
+	@Autowired
+	private CorpKfAccountSessionConfigMapper corpKfAccountSessionConfigMapper;
+
+	@Autowired
+	private CorpKfWaitingQueueUserMapper corpKfWaitingQueueUserMapper;
+
+	@Autowired
+	private CorpKfServicerSessionRecordMapper corpKfServicerSessionRecordMapper;
+
+	@Autowired
+	private CorpKfAccountSyncMsgProgressMapper corpKfAccountSyncMsgProgressMapper;
+
+	@Autowired
+	private CorpKfAccountSessionChatMapper corpKfAccountSessionChatMapper;
+
+	@Autowired
+	private CorpKfUserMapper corpKfUserMapper;
+
 	@PostConstruct
 	private void init() {
 		corpMessageRouter = new CorpMessageRouter(this)
@@ -208,6 +238,16 @@ public class CorpServiceImpl implements CorpService {
 					}
 				})
 				.end()
+				//企业微信客服 用户进入会话事件
+				.rule().async(false).msgType("event").event("kf_msg_or_event")
+				.handler(new CorpMessageHandler() {
+					@Override
+					public CorpXmlOutMessage handle(CorpXmlMessage message, Map<String, Object> context, CorpService corpService) throws Exception {
+						//
+						return kfMsgOrEvent(message, context);
+					}
+				})
+				.end()
 		;
 
 	}
@@ -489,4 +529,544 @@ public class CorpServiceImpl implements CorpService {
 		}
 
 	}
+
+	/**
+	 * 企业微信客服 时间消息回调
+	 */
+	private CorpXmlOutMessage kfMsgOrEvent(CorpXmlMessage message, Map<String, Object> context) {
+		try {
+			String openKfId = message.getOpenKfId();
+			CorpKfAccount corpKfAccount = corpKfAccountMapper.selectOne(Wrappers.lambdaQuery(CorpKfAccount.class)
+					.eq(CorpKfAccount::getOpenKfId, openKfId)
+					.last("limit 1"));
+			if (corpKfAccount == null) {
+				log.info("客服账号:{}已被删除或不存在", openKfId);
+				return null;
+			}
+			//账号会话设置
+			CorpKfAccountSessionConfig corpKfAccountSessionConfig = Optional.ofNullable(corpKfAccountSessionConfigMapper.selectOne(Wrappers.lambdaQuery(CorpKfAccountSessionConfig.class)
+					.eq(CorpKfAccountSessionConfig::getOpenKfId, openKfId)
+					.last("limit 1"))).orElse(new CorpKfAccountSessionConfig());
+			//同步消息进度
+			CorpKfAccountSyncMsgProgress corpKfAccountSyncMsgProgress = Optional.ofNullable(corpKfAccountSyncMsgProgressMapper.selectOne(Wrappers.lambdaQuery(CorpKfAccountSyncMsgProgress.class)
+					.eq(CorpKfAccountSyncMsgProgress::getOpenKfId, openKfId)
+					.last("limit 1"))).orElse(new CorpKfAccountSyncMsgProgress());
+			String cursor = null;
+			if (corpKfAccountSyncMsgProgress.getNextCursor() != null) {
+				cursor = corpKfAccountSyncMsgProgress.getNextCursor();
+			}
+			String corpId = message.getToUserName();
+			String token = message.getToken();
+			syncCorpKfMsg(corpKfAccount, corpKfAccountSessionConfig, corpId, cursor, token, null, corpKfAccountSyncMsgProgress);
+			corpKfAccountSyncMsgProgress.setOpenKfId(openKfId);
+			//同步记录
+			if (corpKfAccountSyncMsgProgress.getId() == null) {
+				corpKfAccountSyncMsgProgressMapper.insert(corpKfAccountSyncMsgProgress);
+			} else corpKfAccountSyncMsgProgressMapper.updateById(corpKfAccountSyncMsgProgress);
+		} catch (Exception e) {
+			log.error("企业微信客服 事件消息回调 错误,e:{}", StringUtil.getErrorText(e));
+		}
+		return null;
+	}
+
+	private void syncCorpKfMsg(CorpKfAccount corpKfAccount, CorpKfAccountSessionConfig corpKfAccountSessionConfig, String corpId, String cursor, String token, Integer limit, CorpKfAccountSyncMsgProgress corpKfAccountSyncMsgProgress) throws Exception {
+		String openKfId = corpKfAccount.getOpenKfId();
+		WxCpKfMsgListResp wxCpKfMsgListResp = wxCorpOps.syncMsg(corpId, cursor, token, limit, openKfId);
+		List<WxCpKfMsgListResp.WxCpKfMsgItem> msgList = wxCpKfMsgListResp.getMsgList();
+		for (WxCpKfMsgListResp.WxCpKfMsgItem item : msgList) {
+			if (item.getEvent() != null) {
+				//会话事件
+				if (item.getEvent().getEventType().equals("enter_session")) {
+					//进入会话事件
+					enterSessionEvent(corpKfAccount, corpKfAccountSessionConfig, corpId, item);
+				}
+				//会话变更事件
+				if (item.getEvent().getEventType().equals("session_status_change")) {
+					//会话更变事件
+					sessionStatusChange(corpId, openKfId, item, corpKfAccountSessionConfig);
+				}
+			}
+			//同步微信客服账号信息
+			syncKfChatMsg(corpId, openKfId, corpKfAccountSessionConfig, item);
+		}
+		cursor = wxCpKfMsgListResp.getNextCursor();
+		corpKfAccountSyncMsgProgress.setNextCursor(cursor);
+		//同步信息
+		while (wxCpKfMsgListResp.getHasMore() == 1) {
+			syncCorpKfMsg(corpKfAccount, corpKfAccountSessionConfig, corpId, wxCpKfMsgListResp.getNextCursor(), token, limit, corpKfAccountSyncMsgProgress);
+		}
+	}
+
+	private void syncKfChatMsg(String corpId, String openKfId, CorpKfAccountSessionConfig corpKfAccountSessionConfig, WxCpKfMsgListResp.WxCpKfMsgItem item) throws Exception {
+		CorpKfAccountSessionChat corpKfAccountSessionChat = new CorpKfAccountSessionChat();
+		Boolean record = false;
+		//文本
+		if (item.getText() != null) {
+			corpKfAccountSessionChat.setMsgContent(item.getText().getContent());
+			record = true;
+		} else if (item.getMergedMsg() != null) {
+			//聊天记录 转发
+			corpKfAccountSessionChat.setMsgContent(Jsons.toJson(item.getMergedMsg().getItem()));
+			record = true;
+		} else if (item.getVideo() != null) {
+			//视频记录
+			setMediaUrl(corpKfAccountSessionChat, item.getVideo().getMedia_id());
+			record = true;
+		} else if (item.getFile() != null) {
+			//文件
+			setMediaUrl(corpKfAccountSessionChat, item.getFile().getMedia_id());
+			record = true;
+		} else if (item.getImage() != null) {
+			//图片
+			setMediaUrl(corpKfAccountSessionChat, item.getImage().getMedia_id());
+			record = true;
+		} else if (item.getLink() != null) {
+			//链接
+			WxCorpLinkMsg link = item.getLink();
+			corpKfAccountSessionChat.setLinkTitle(link.getTitle());
+			corpKfAccountSessionChat.setLinkDesc(link.getDesc());
+			corpKfAccountSessionChat.setLinkUrl(link.getUrl());
+			corpKfAccountSessionChat.setLinkPicUrl(link.getPic_url());
+			record = true;
+		} else if (item.getMsgmenu() != null) {
+			//菜单 结束语统计
+			corpKfAccountSessionChat.setMsgContent(Jsons.toJson(item.getMsgmenu()));
+			record = true;
+		}
+		if (record) {
+			//新增
+			String openKfid = item.getOpenKfid();
+			//客户id
+			String externalUserId = item.getExternalUserId();
+			//接待人员id
+			String servicerUserId = item.getServicerUserId();
+			//消息来源。3-微信客户发送的消息 4-系统推送的事件消息 5-接待人员在企业微信客户端发送的消息
+			Integer origin = item.getOrigin();
+			//发送时间
+			Long sendTime = item.getSendTime();
+			//消息id
+			String msgId = item.getMsgId();
+			//消息类型
+			String msgType = item.getMsgType();
+			corpKfAccountSessionChat.setOpenKfId(openKfid);
+			corpKfAccountSessionChat.setMsgid(msgId);
+			corpKfAccountSessionChat.setServicerUserid(servicerUserId);
+			corpKfAccountSessionChat.setExternalUserid(externalUserId);
+			corpKfAccountSessionChat.setOrign(origin);
+			corpKfAccountSessionChat.setMsgType(msgType);
+			corpKfAccountSessionChat.setSendTime(sendTime);
+			corpKfAccountSessionChat.setSendDate(DateTime.of(sendTime * 1000));
+			saveChatMsg(corpKfAccountSessionChat);
+
+			if (StrUtil.isNotEmpty(externalUserId)) {
+				//会话状态为待处理 则分配接待人员
+				assignKfSession(corpId, openKfId, externalUserId, corpKfAccountSessionConfig, corpKfAccountSessionChat.getSendDate(), msgId);
+			}
+			syncSessionSatisfaction(item);
+		}
+	}
+
+	private void syncSessionSatisfaction(WxCpKfMsgListResp.WxCpKfMsgItem item) throws Exception {
+		Integer satisfaction = null;
+		WxCorpTextMsg text = item.getText();
+		//自定义菜单id
+		if (text != null && text.getMenu_id() != null) {
+			String[] split = text.getMenu_id().split("-");
+			if (split.length < 2) {
+				return;
+			}
+			String menuId = split[0];
+			String msgid = split[1];
+			//满意
+			if ("101".equals(menuId)) {
+				satisfaction = 1;
+			}
+			//一般
+			if ("102".equals(menuId)) {
+				satisfaction = 2;
+			}
+			//不满意
+			if ("103".equals(menuId)) {
+				satisfaction = 3;
+			}
+			if (satisfaction != null) {
+				CorpKfServicerSessionRecord corpKfServicerSessionRecord = corpKfServicerSessionRecordMapper.selectOne(Wrappers.lambdaQuery(CorpKfServicerSessionRecord.class)
+						.eq(CorpKfServicerSessionRecord::getMsgid, msgid)
+						.last("limit 1"));
+				if (corpKfServicerSessionRecord != null) {
+					corpKfServicerSessionRecord.setSatisfaction(satisfaction);
+					corpKfServicerSessionRecordMapper.updateById(corpKfServicerSessionRecord);
+					//设置会话id
+					corpKfAccountSessionChatMapper.update(null, Wrappers.lambdaUpdate(CorpKfAccountSessionChat.class)
+							.set(CorpKfAccountSessionChat::getSessionId, corpKfServicerSessionRecord.getId())
+							.eq(CorpKfAccountSessionChat::getMsgid, item.getMsgId()));
+				}
+			}
+		}
+	}
+
+	private void sessionStatusChange(String corpId, String openKfId, WxCpKfMsgListResp.WxCpKfMsgItem wxCpKfMsgItem, CorpKfAccountSessionConfig corpKfAccountSessionConfig) throws Exception {
+		WxCpKfEventMsg event = wxCpKfMsgItem.getEvent();
+		Integer changeType = event.getChangeType();
+		String externalUserId = event.getExternalUserId();
+		Long sendTime = wxCpKfMsgItem.getSendTime();
+		DateTime sendDate = DateTime.of(sendTime * 1000);
+		//1-从接待池接入会话 2-转接会话 3-结束会话 4-重新接入已结束/已转接会话
+		String oldServicerUserId = event.getOldServicerUserId();
+		String newServicerUserId = event.getNewServicerUserId();
+		String msgId = wxCpKfMsgItem.getMsgId();
+		String msgCode = wxCpKfMsgItem.getEvent().getMsgCode();
+		if (changeType == 1) {
+			CorpKfServicerSessionRecord newSession = new CorpKfServicerSessionRecord();
+			newSession.setOpenKfId(openKfId);
+			newSession.setServicerUserid(newServicerUserId);
+			newSession.setExternalUserid(externalUserId);
+			newSession.setChangeType(changeType);
+			newSession.setServiceState(0);
+			newSession.setStartTime(sendDate);
+			newSession.setMsgid(msgId);
+			corpKfServicerSessionRecordMapper.insert(newSession);
+			//等待用户删除
+			delQueueUser(openKfId, externalUserId, sendDate);
+			syncUserChatSession(newSession.getId(), externalUserId);
+		}
+		//2-转接会话
+		if (changeType == 2) {
+			//设置表中会话状态
+			setDbSessionState(openKfId, oldServicerUserId, externalUserId, sendDate, changeType, msgId, msgCode);
+			//记录新的客服会话
+			String newServicerUserid = wxCpKfMsgItem.getNewServicerUserid();
+			transCorpKfSessionToServicer(corpId, openKfId, oldServicerUserId, newServicerUserid, externalUserId, changeType, 3, sendDate, msgId);
+		}
+		//3-结束会话 4-重新接入已结束/已转接会话
+		if (changeType == 3 || changeType == 4) {
+			setDbSessionState(openKfId, oldServicerUserId, externalUserId, sendDate, changeType, msgId, msgCode);
+		}
+	}
+
+	/**
+	 * enter_session 会话事件
+	 */
+	public void enterSessionEvent(CorpKfAccount corpKfAccount, CorpKfAccountSessionConfig corpKfAccountSessionConfig, String corpId, WxCpKfMsgListResp.WxCpKfMsgItem wxCpKfMsgItem) throws Exception {
+		String openKfId = corpKfAccount.getOpenKfId();
+		DateTime sendDate = DateTime.of(wxCpKfMsgItem.getSendTime() * 1000);
+		WxCpKfEventMsg event = wxCpKfMsgItem.getEvent();
+		String welcomeCode = event.getWelcomeCode();
+		String msgid = wxCpKfMsgItem.getMsgId();
+		//如果满足发送欢迎语条件(条件为:用户在过去48小时里未收过欢迎语,且未向客服发过消息),会返回该字段。
+		//可用该welcome_code调用发送事件响应消息接口给客户发送欢迎语。
+		if (StrUtil.isNotEmpty(welcomeCode)) {
+			WxCorpKfMsgSendRequest wxCpKfMsgSendRequest = new WxCorpKfMsgSendRequest();
+			wxCpKfMsgSendRequest.setMsgType("text");
+			wxCpKfMsgSendRequest.setCode(welcomeCode);
+			WxCorpKfMsgSendRequest.WxCpKfTextMsg textMsg = new WxCorpKfMsgSendRequest.WxCpKfTextMsg();
+			if (StrUtil.isNotEmpty(corpKfAccountSessionConfig.getWelcomeContent())) {
+				textMsg.setContent(corpKfAccountSessionConfig.getWelcomeContent());
+				wxCpKfMsgSendRequest.setText(textMsg);
+				wxCpKfMsgSendRequest.setToUser(welcomeCode);
+				wxCpKfMsgSendRequest.setOpenKfid(event.getOpenKfid());
+				try {
+					wxCorpOps.sendMsgOnEvent(corpId, wxCpKfMsgSendRequest);
+					//同步用户信息
+					syncCorpkfUserInfo(event.getExternalUserId());
+				} catch (Exception e) {
+					log.error("发送微信客服欢迎语失败:{}", StringUtil.getErrorText(e));
+				}
+			}
+		}
+		//分配接待人员
+		assignKfSession(corpId, openKfId, event.getExternalUserId(), corpKfAccountSessionConfig, sendDate, msgid);
+	}
+
+	/**
+	 * 同步用户信息
+	 */
+	private void syncCorpkfUserInfo(String externalUserid) throws Exception {
+		CorpKfUserInfo corpKfUserInfo = wxCorpOps.getCorpKfUserInfo(externalUserid);
+		CorpKfUser corpKfUser = corpKfUserMapper.selectOne(Wrappers.lambdaQuery(CorpKfUser.class)
+				.eq(CorpKfUser::getExternalUserid, externalUserid)
+				.last("limit 1"));
+		if (corpKfUser == null) {
+			try {
+				corpKfUser = new CorpKfUser();
+				BeanUtil.copyProperties(corpKfUserInfo, corpKfUser);
+				corpKfUserMapper.insert(corpKfUser);
+			} catch (Exception e) {
+			}
+		} else {
+			corpKfUser.setAvatar(corpKfUserInfo.getAvatar());
+			corpKfUser.setNickname(corpKfUserInfo.getNickname());
+			corpKfUser.setGender(corpKfUserInfo.getGender());
+			corpKfUserMapper.updateById(corpKfUser);
+		}
+	}
+
+	/**
+	 * 变更用户会话状态
+	 * serviceState 0未处理 1由智能助手接待 2待接入池排队中 3由人工接待 4 已结束/未开始
+	 */
+	private void transCorpKfSessionToServicer(String corpId, String openKfId, String oldServicerUserId, String userid, String externalUserId, Integer changeType, Integer serviceState, Date sendTime, String msgid) throws Exception {
+		//记录接待人员会话
+		CorpKfServicerSessionRecord corpKfServicerSessionRecord = new CorpKfServicerSessionRecord();
+		corpKfServicerSessionRecord.setOpenKfId(openKfId);
+		corpKfServicerSessionRecord.setServiceState(0);
+		corpKfServicerSessionRecord.setFromUserid(oldServicerUserId);
+		corpKfServicerSessionRecord.setServicerUserid(userid);
+		corpKfServicerSessionRecord.setExternalUserid(externalUserId);
+		corpKfServicerSessionRecord.setChangeType(changeType);
+		corpKfServicerSessionRecord.setStartTime(sendTime);
+		corpKfServicerSessionRecord.setMsgid(msgid);
+		corpKfServicerSessionRecordMapper.insert(corpKfServicerSessionRecord);
+		//调用微信客服接口 变更会话状态
+		wxCorpOps.transCorpKfSessionToServicer(corpId, openKfId, userid, externalUserId, serviceState);
+		//等待用户删除
+		delQueueUser(openKfId, externalUserId, sendTime);
+		//同步这一次接待记录
+		if (serviceState == 3) {
+			corpKfAccountSessionChatMapper.update(null, Wrappers.lambdaUpdate(CorpKfAccountSessionChat.class)
+					.set(CorpKfAccountSessionChat::getSessionId, corpKfServicerSessionRecord.getId())
+					.eq(CorpKfAccountSessionChat::getOpenKfId, openKfId)
+					.eq(CorpKfAccountSessionChat::getExternalUserid, externalUserId)
+					.isNull(CorpKfAccountSessionChat::getSessionId));
+		}
+	}
+
+	/**
+	 * 发送微信客服会话文本消息
+	 */
+	public void sendCorpKfTextTips(String corpId, String externalUserid, String openKfId, String content) throws Exception {
+		if (StrUtil.isEmpty(content)) {
+			return;
+		}
+		//查询会话状态
+		ServiceStateResponse serviceStateResponse = wxCorpOps.getCorpKfSessionStatus(corpId, openKfId, externalUserid);
+		Integer state = serviceStateResponse.getService_state();
+		//0.未处理 1.人工智能接待 可调用api发送消息
+		if (state == null && state != 0 && state != 1) {
+			log.info("目前openKfId:{}用户eid:{}会话处于状态中state:{},无法发送消息", openKfId, externalUserid, state);
+			return;
+		}
+		WxCorpKfMsgSendRequest wxCpKfMsgSendRequest = new WxCorpKfMsgSendRequest();
+		wxCpKfMsgSendRequest.setMsgType("text");
+		wxCpKfMsgSendRequest.setToUser(externalUserid);
+		wxCpKfMsgSendRequest.setOpenKfid(openKfId);
+		WxCorpKfMsgSendRequest.WxCpKfTextMsg textMsg = new WxCorpKfMsgSendRequest.WxCpKfTextMsg();
+		textMsg.setContent(content);
+		wxCpKfMsgSendRequest.setText(textMsg);
+		wxCorpOps.sendTextMsg(corpId, wxCpKfMsgSendRequest);
+	}
+
+	/**
+	 * 会话状态为待处理 则分配接待人员
+	 */
+	public void assignKfSession(String corpId, String openKfId, String externalUserid, CorpKfAccountSessionConfig corpKfAccountSessionConfig, Date sendDate, String msgid) throws Exception {
+		//超过15分钟 进入会话时间不处理
+		if (sendDate.compareTo(DateUtil.offsetMinute(DateTime.now(), -15)) <= 0) {
+			return;
+		}
+		//查询用户会话状态 0待处理 	1智能助手接待
+		ServiceStateResponse serviceStateResponse = wxCorpOps.getCorpKfSessionStatus(corpId, openKfId, externalUserid);
+		Integer state = serviceStateResponse.getService_state();
+		if (state != 0 && state != 1) {
+			return;
+		}
+		//当前时间段值班客服
+		Set<String> dutyFollowIds = getDutyFollowIds(openKfId);
+		//非值班
+		if (dutyFollowIds.isEmpty()) {
+			//非值班时间 移入等待池
+			log.info("当前客服账号:{}处于非值班时间:{},将客户:{}移入等待池", openKfId, DateTime.now(), externalUserid);
+			//发送进入等待池消息
+			sendCorpKfTextTips(corpId, externalUserid, openKfId, corpKfAccountSessionConfig.getQueueTips());
+			moveWaitingQueue(corpId, openKfId, externalUserid);
+			return;
+		}
+		//获取接待人员列表
+		CorpKfServicerResp corpKfServicerResp = wxCorpOps.getKfServicerList(corpId, openKfId);
+		List<CorpKfServicerResp.CorpKfServicer> servicerList = corpKfServicerResp.getServicerList();
+		//目前值班客服id
+		List<String> corpKfServiceIds = new ArrayList<>();
+		servicerList.forEach(servicer -> {
+			if (servicer.getUserid() != null && servicer.getStatus() == 0 && dutyFollowIds.contains(servicer.getUserid())) {
+				corpKfServiceIds.add(servicer.getUserid());
+			}
+		});
+		//客服是否已经开始值班
+		//没有值班人员
+		if (CollUtil.isEmpty(corpKfServiceIds)) {
+			log.info("当前客服账号:{}无值班人员,将客户:{}移入等待池", openKfId, externalUserid);
+			sendCorpKfTextTips(corpId, externalUserid, openKfId, corpKfAccountSessionConfig.getQueueTips());
+			//接入等待池
+			moveWaitingQueue(corpId, openKfId, externalUserid);
+			return;
+		}
+		//是否有空闲接待人员
+		List<CorpKfServicerDto> corpKfServicerDtos = corpKfServicerSessionRecordMapper.getAvailableServicer(openKfId);
+		//暂无分配会话 初始化
+		if (CollUtil.isEmpty(corpKfServicerDtos)) {
+			//随机分配一个客服
+			String servicerUserid = corpKfServiceIds.get(RandomUtil.randomInt(corpKfServiceIds.size()));
+			//变更会话状态
+			transCorpKfSessionToServicer(corpId, openKfId, null, servicerUserid, externalUserid, null, 3, sendDate, msgid);
+			return;
+		}
+		//移入未接入的客服
+		List<String> collect = corpKfServicerDtos.stream().map(CorpKfServicerDto::getServicerUserid).collect(Collectors.toList());
+		corpKfServiceIds.forEach(serviceId -> {
+			if (!collect.contains(serviceId)) {
+				CorpKfServicerDto corpKfServicerDto = new CorpKfServicerDto();
+				corpKfServicerDto.setServicerUserid(serviceId);
+				corpKfServicerDto.setNum(0);
+				corpKfServicerDtos.add(corpKfServicerDto);
+			}
+		});
+		//筛选符合条件的客服
+		//默认接入最少的优先分配 接待人最少的在集合前面
+		//不超过最多接待人数 默认30
+		Integer maxNum = Optional.ofNullable(corpKfAccountSessionConfig.getMaxNum()).orElse(30);
+		List<CorpKfServicerDto> filterKfCs = corpKfServicerDtos.stream().filter(kfCs -> corpKfServiceIds.contains(kfCs.getServicerUserid()) && kfCs.getNum() < maxNum).collect(Collectors.toList());
+		if (CollUtil.isEmpty(filterKfCs)) {
+			log.info("当前客服账号:{}值班接待上限:{},将客户:{}移入等待池", openKfId, maxNum, externalUserid);
+			//发送进入等待池消息
+			sendCorpKfTextTips(corpId, externalUserid, openKfId, corpKfAccountSessionConfig.getQueueTips());
+			//移入等待池
+			moveWaitingQueue(corpId, openKfId, externalUserid);
+			return;
+		}
+		//选择第一个分配接待
+		CorpKfServicerDto corpKfServicerDto = filterKfCs.get(0);
+		//变更会话状态
+		transCorpKfSessionToServicer(corpId, openKfId, null, corpKfServicerDto.getServicerUserid(), externalUserid, null, 3, sendDate, msgid);
+	}
+
+	private void moveWaitingQueue(String corpId, String openKfId, String externalUserId) throws Exception {
+		CorpKfWaitingQueueUser corpKfWaitingQueueUser = new CorpKfWaitingQueueUser();
+		corpKfWaitingQueueUser.setOpenKfId(openKfId);
+		corpKfWaitingQueueUser.setExternalUserid(externalUserId);
+		try {
+			corpKfWaitingQueueUserMapper.insert(corpKfWaitingQueueUser);
+			//移入等待池
+			wxCorpOps.moveWaitingQueue(corpId, openKfId, externalUserId);
+		} catch (DuplicateKeyException e) {
+
+		}
+	}
+
+	/**
+	 * 设置临时素材url
+	 */
+	private void setMediaUrl(CorpKfAccountSessionChat corpKfAccountSessionChat, String mediaId) throws Exception {
+		String url = wxCorpOps.getMedia(null, mediaId);
+		corpKfAccountSessionChat.setUrl(url);
+	}
+
+	/**
+	 * 新增记录
+	 */
+	public void saveChatMsg(CorpKfAccountSessionChat chat) {
+		CorpKfServicerSessionRecord corpKfServicerSessionRecord = corpKfServicerSessionRecordMapper.selectOne(Wrappers.lambdaQuery(CorpKfServicerSessionRecord.class)
+				.eq(CorpKfServicerSessionRecord::getOpenKfId, chat.getOpenKfId())
+				.eq(CorpKfServicerSessionRecord::getExternalUserid, chat.getExternalUserid())
+				.eq(CorpKfServicerSessionRecord::getServiceState, 0)
+				.last("limit 1"));
+		if (corpKfServicerSessionRecord != null) chat.setSessionId(corpKfServicerSessionRecord.getId());
+		corpKfAccountSessionChatMapper.insert(chat);
+	}
+
+	/**
+	 * 设置会话记录状态
+	 */
+	public void setDbSessionState(String openKfId, String servicerUserid, String externalUserId, Date sendDate, Integer changeType, String msgid, String msgCode) throws Exception {
+		CorpKfServicerSessionRecord corpKfServicerSessionRecord = corpKfServicerSessionRecordMapper.selectOne(Wrappers.lambdaQuery(CorpKfServicerSessionRecord.class)
+				.eq(CorpKfServicerSessionRecord::getOpenKfId, openKfId)
+				.eq(CorpKfServicerSessionRecord::getServicerUserid, servicerUserid)
+				.eq(CorpKfServicerSessionRecord::getExternalUserid, externalUserId)
+				.eq(CorpKfServicerSessionRecord::getServiceState, 0)
+				.last("limit 1"));
+		if (corpKfServicerSessionRecord != null) {
+			//同步用户该次接待记录
+			syncUserChatSession(corpKfServicerSessionRecord.getId(), externalUserId);
+			corpKfServicerSessionRecord.setCloseTime(sendDate);
+			corpKfServicerSessionRecord.setServiceState(changeType != 2 ? 1 : changeType);
+			if (changeType != 2) {
+				try {
+					wxCorpOps.sendSatisfactionMsg(corpKfServicerSessionRecord.getMsgid(), openKfId, externalUserId, msgCode);
+					corpKfServicerSessionRecord.setSendSatisfaction(true);
+				} catch (Exception e) {
+					log.error("发送满意度调查内容错误:{}", StringUtil.getErrorText(e));
+				}
+			}
+			corpKfServicerSessionRecordMapper.updateById(corpKfServicerSessionRecord);
+		} else {
+			corpKfServicerSessionRecord = new CorpKfServicerSessionRecord();
+			corpKfServicerSessionRecord.setOpenKfId(openKfId);
+			corpKfServicerSessionRecord.setServicerUserid(servicerUserid);
+			corpKfServicerSessionRecord.setExternalUserid(externalUserId);
+			corpKfServicerSessionRecord.setServiceState(changeType != 2 ? 1 : changeType);
+			corpKfServicerSessionRecord.setMsgid(msgid);
+			corpKfServicerSessionRecord.setCloseTime(sendDate);
+			corpKfServicerSessionRecordMapper.insert(corpKfServicerSessionRecord);
+			//同步用户该次接待记录
+			syncUserChatSession(corpKfServicerSessionRecord.getId(), externalUserId);
+			//同步会话最早德发送时间
+			CorpKfAccountSessionChat beginChat = corpKfAccountSessionChatMapper.selectOne(Wrappers.lambdaQuery(CorpKfAccountSessionChat.class)
+					.eq(CorpKfAccountSessionChat::getSessionId, corpKfServicerSessionRecord.getId())
+					.eq(CorpKfAccountSessionChat::getExternalUserid, externalUserId)
+					.orderByAsc(CorpKfAccountSessionChat::getId)
+					.last("limit 1"));
+			if (beginChat != null) {
+				corpKfServicerSessionRecord.setStartTime(beginChat.getSendDate());
+				corpKfServicerSessionRecordMapper.updateById(corpKfServicerSessionRecord);
+			}
+		}
+	}
+
+	/**
+	 * 删除等待池中用户
+	 *
+	 * @param openKfId
+	 * @param externalUserid
+	 */
+	public void delQueueUser(String openKfId, String externalUserid, Date sendDate) {
+		//等待用户删除
+		Optional.ofNullable(corpKfWaitingQueueUserMapper.selectOne(Wrappers.lambdaQuery(CorpKfWaitingQueueUser.class)
+				.eq(CorpKfWaitingQueueUser::getOpenKfId, openKfId)
+				.eq(CorpKfWaitingQueueUser::getExternalUserid, externalUserid)
+				.le(CorpKfWaitingQueueUser::getCreatedTime, sendDate)
+				.last("limit 1"))).ifPresent(queueUser -> corpKfWaitingQueueUserMapper.deleteById(queueUser.getId()));
+	}
+
+	/**
+	 * 获取当前值班客服
+	 */
+	public Set<String> getDutyFollowIds(String openKfId) throws Exception {
+		DateTime now = DateTime.now();
+		String thisHourDate = DateUtil.format(now, "HH:mm");
+		//星期天为1
+		int dayOfWeek = DateUtil.dayOfWeek(now);
+		//是否值班期
+		List<CorpKfAccountFollow> corpKfAccountFollows = corpKfAccountFollowMapper.selectList(Wrappers.lambdaQuery(CorpKfAccountFollow.class)
+				.eq(CorpKfAccountFollow::getOpenKfId, openKfId)
+				.le(CorpKfAccountFollow::getStartTime, thisHourDate)
+				.ge(CorpKfAccountFollow::getEndTime, thisHourDate)
+				.eq(CorpKfAccountFollow::getDutyWeek, dayOfWeek));
+
+		//目前时间段需要值班的客服
+		Set<String> thisHourDutyFollows = new HashSet<>();
+		for (CorpKfAccountFollow corpKfAccountFollow : corpKfAccountFollows) {
+			thisHourDutyFollows.addAll(Jsons.parseList(corpKfAccountFollow.getUserIdList(), String.class));
+		}
+		return thisHourDutyFollows;
+	}
+
+	/**
+	 * 同步接待用户聊天会话id
+	 */
+	public void syncUserChatSession(Long sessionId, String externalUserid) {
+		//同步用户该次接待记录
+		corpKfAccountSessionChatMapper.update(null, Wrappers.lambdaUpdate(CorpKfAccountSessionChat.class)
+				.set(CorpKfAccountSessionChat::getSessionId, sessionId)
+				.eq(CorpKfAccountSessionChat::getExternalUserid, externalUserid)
+				.isNull(CorpKfAccountSessionChat::getSessionId));
+	}
 }

+ 378 - 39
netflix-service/src/main/java/com/cyksj/service/corp/impl/WxCorpOpsImpl.java

@@ -1,6 +1,7 @@
 package com.cyksj.service.corp.impl;
 
 import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.RandomUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HttpUtil;
 import cn.hutool.json.JSONObject;
@@ -15,7 +16,7 @@ import com.cyksj.config.corp.YHLXWxCorpConfig;
 import com.cyksj.dto.CorpOauth2UserInfo;
 import com.cyksj.dto.RedisKey;
 import com.cyksj.model.entity.WxCorpApp;
-import com.cyksj.model.kf.CorpKfUserInfo;
+import com.cyksj.model.kf.*;
 import com.cyksj.model.request.corp.CorpFollowContact;
 import com.cyksj.model.request.corp.CorpGroupConfig;
 import com.cyksj.model.request.corp.CorpMsgTemplate;
@@ -36,14 +37,14 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import javax.imageio.stream.FileImageOutputStream;
 import java.io.File;
 import java.net.URLEncoder;
 import java.net.http.HttpRequest;
 import java.net.http.HttpResponse;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
 
 /*
  *项目名: netflix
@@ -60,6 +61,9 @@ public class WxCorpOpsImpl implements WxCorpOps {
 	@Autowired
 	private RedisService redisService;
 
+	//微信客服满意度
+	private static final String SATISFACTION = "{\"touser\":\"EXTERNAL_USERID\",\"open_kfid\":\"OPEN_KFID\",\"msgid\":\"MSGID\",\"msgtype\":\"msgmenu\",\"msgmenu\":{\"head_content\":\"本次服务已结束,如有其他需要可重新发起对话。\\n您对本次服务是否满意呢? \",\"list\":[{\"type\":\"click\",\"click\":{\"id\":\"101-%s\",\"content\":\"满意\"}},{\"type\":\"click\",\"click\":{\"id\":\"102-%s\",\"content\":\"一般\"}},{\"type\":\"click\",\"click\":{\"id\":\"103-%s\",\"content\":\"不满意\"}}]}}";
+
 	@Override
 	public CorpExternalContactInfo getExternalContact(String corpId, String userId) throws Exception {
 		String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=" + getAccessToken(corpId) + "&external_userid=" + userId;
@@ -92,7 +96,7 @@ public class WxCorpOpsImpl implements WxCorpOps {
 		if (StringUtils.isNotBlank(accessToken)) {
 			return accessToken;
 		}
-		return getKfAccessToken(corpId);
+		return getAccessToken(corpId);
 	}
 
 	@Override
@@ -647,39 +651,6 @@ public class WxCorpOpsImpl implements WxCorpOps {
 		redisService.set(RedisKey.CORP_ACCESS_TOKEN_KEY + corpId, accessToken, token.getExpiresIn() - 300L);
 		return accessToken;
 	}
-
-	public String getKfAccessToken(String corpId) throws Exception {
-		WxCorpApp wxCorpApp = WeChatCorpFactory.getCorpConfigStrategy(corpId);
-		if (StringUtils.isBlank(wxCorpApp.getMsgSecret())) {
-			throw BusinessRuntimeException.getInstance("该企业会话存档权限尚未开通.请联系管理员配置api secret");
-		}
-		String accessToken = redisService.getStr(RedisKey.CORP_MSG_ACCESS_TOKEN_KEY + corpId);
-		if(StringUtils.isBlank(accessToken)){
-			String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken" +
-					"?corpid=" + corpId +"&corpsecret=" + wxCorpApp.getMsgSecret();
-
-			HttpResponse<String> res =
-					J11HttpC.custom()
-							.ofGet()
-							.url(url)
-							.send(HttpResponse.BodyHandlers.ofString());
-
-			if (200 != res.statusCode()) {
-				throw BusinessRuntimeException.getInstance("获取企业微信access_token失败: " + res.body());
-			}
-			JSONObject result = new JSONObject(res.body());
-
-			final Integer errcode = result.getInt("errcode");
-			if (null != errcode && 0 != errcode) {
-				throw BusinessRuntimeException.getInstance("获取企业微信access_token失败: " + result.getStr("errmsg") + "[" + errcode + "]");
-			}
-			accessToken = result.getStr("access_token");
-			redisService.set(RedisKey.CORP_MSG_ACCESS_TOKEN_KEY + corpId, accessToken, result.getInt("expires_in") - 300L);
-			return accessToken;
-		}
-		return accessToken;
-	}
-
 	public String js_ticket(String corpId) throws Exception {
 		String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=%s", this.getAccessToken(corpId));
 		HttpResponse<byte[]> res =
@@ -828,6 +799,228 @@ public class WxCorpOpsImpl implements WxCorpOps {
 		return JSONUtil.parseObj(res.body()).getJSONArray("ids").toList(String.class);
 	}
 
+	@Override
+	public WxCpKfAccountAddResp addAccount(String corpId, WxCpKfAccountAdd add) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/add?access_token=" + getAccessToken(corpId);
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(add), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("添加客服帐号: " + res.body());
+		}
+
+		WxCpKfAccountAddResp resp = Jsons.parseObject(res.body(), WxCpKfAccountAddResp.class);
+		if (resp.getErrcode() != 0) {
+			throw BusinessRuntimeException.getInstance("添加客服帐号: " + resp.getErrmsg());
+		}
+		return resp;
+	}
+
+	@Override
+	public void delAccount(String corpId, WxCpKfAccountDel del) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/del?access_token=" + getAccessToken(corpId);
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(del), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("删除客服帐号: " + res.body());
+		}
+
+		JSONObject jsonObject = new JSONObject(res.body());
+		if (jsonObject.getInt("errcode") != 0) {
+			throw BusinessRuntimeException.getInstance("删除客服账号出错: " + jsonObject.getStr("errmsg"));
+		}
+	}
+
+	@Override
+	public void updAccount(String corpId, WxCpKfAccountUpd upd) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/account/update?access_token=" + getAccessToken(corpId);
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(upd), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("修改客服帐号: " + res.body());
+		}
+
+		JSONObject jsonObject = new JSONObject(res.body());
+		if (jsonObject.getInt("errcode") != 0) {
+			throw BusinessRuntimeException.getInstance("修改客服账号出错: " + jsonObject.getStr("errmsg"));
+		}
+	}
+
+	@Override
+	public WxCpKfMsgListResp syncMsg(String corpId, String cursor, String token, Integer limit, String openKfId) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/sync_msg?access_token=" + getAccessToken(corpId);
+
+		JSONObject json = new JSONObject();
+		json.putOpt("open_kfid", openKfId);
+		if (cursor != null) {
+			json.putOpt("cursor", cursor);
+		}
+		if (token != null) {
+			json.putOpt("token", token);
+		}
+		if (limit != null) {
+			json.putOpt("limit", limit);
+		}
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(json), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("读取消息: " + res.body());
+		}
+
+		WxCpKfMsgListResp resp = Jsons.parseObject(res.body(), WxCpKfMsgListResp.class);
+		if (resp.getErrcode() != 0) {
+			throw BusinessRuntimeException.getInstance("读取消息错误: " + resp.getErrmsg());
+		}
+		return resp;
+	}
+
+	@Override
+	public WxCpKfMsgSendResp sendMsgOnEvent(String corpId, WxCorpKfMsgSendRequest request) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg_on_event?access_token=" + getAccessToken(corpId);
+
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(request), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("发送客服事件响应消息: " + res.body());
+		}
+
+		WxCpKfMsgSendResp resp = Jsons.parseObject(res.body(), WxCpKfMsgSendResp.class);
+		if (resp.getErrcode() != 0) {
+			throw BusinessRuntimeException.getInstance("发送欢迎语等事件响应消息错误: " + resp.getErrmsg());
+		}
+		return resp;
+	}
+
+	@Override
+	public CorpKfServicerResp getKfServicerList(String corpId,String openKfId) throws Exception {
+		String url = String.format("https://qyapi.weixin.qq.com/cgi-bin/kf/servicer/list?access_token=%s&open_kfid=%s", getAccessToken(corpId), openKfId);
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("获取微信客服接待人员响应消息: " + res.body());
+		}
+
+		CorpKfServicerResp resp = Jsons.parseObject(res.body(), CorpKfServicerResp.class);
+		if (resp.getErrcode() != 0) {
+			throw BusinessRuntimeException.getInstance("获取微信客服接待人员响应消息错误: " + resp.getErrmsg());
+		}
+		return resp;
+	}
+
+	@Override
+	public void moveWaitingQueue(String corpId, String openKfId, String externalUserId) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/service_state/trans?access_token=" + getAccessToken(corpId);
+		JSONObject request = new JSONObject();
+		request.putOpt("open_kfid", openKfId);
+		request.putOpt("external_userid", externalUserId);
+		//待接入池排队中
+		request.putOpt("service_state", 2);
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(request), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("移入等待池响应消息: " + res.body());
+		}
+
+		CorpKfSessionResp resp = Jsons.parseObject(res.body(), CorpKfSessionResp.class);
+		if (resp.getErrcode() != 0) {
+			throw BusinessRuntimeException.getInstance("移入等待池响应消息错误: " + resp.getErrmsg());
+		}
+	}
+
+	/**
+	 *
+	 * @param corpId
+	 * @param openKfId
+	 * @param userid
+	 * @param externalUserId
+	 * @param serviceState 0未处理 1由智能助手接待 2待接入池排队中 3由人工接待 4已结束/未开始
+	 * @throws Exception
+	 */
+	@Override
+	public void transCorpKfSessionToServicer(String corpId, String openKfId, String userid, String externalUserId, Integer serviceState) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/service_state/trans?access_token=" + getAccessToken(corpId);
+		JSONObject request = new JSONObject();
+		request.putOpt("open_kfid", openKfId);
+		request.putOpt("external_userid", externalUserId);
+		//由人工接待
+		request.putOpt("service_state", serviceState);
+		request.putOpt("servicer_userid", userid);
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(request), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("客服接入会话响应消息: " + res.body());
+		}
+
+		CorpKfSessionResp resp = Jsons.parseObject(res.body(), CorpKfSessionResp.class);
+		if (resp.getErrcode() != 0) {
+			throw BusinessRuntimeException.getInstance("客服接入会话响应消息错误: " + resp.getErrmsg());
+		}
+	}
+
+	@Override
+	public void sendSatisfactionMsg(String msgid, String openKfId, String externalUserId, String msgCode) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg_on_event?access_token=" + getAccessToken(yhlXWxCorpConfig.getCorpId());
+		CorpKfSatisfactionMsg corpKfSatisfactionMsg = Jsons.parseObject(String.format(SATISFACTION, msgid, msgid, msgid), CorpKfSatisfactionMsg.class);
+		corpKfSatisfactionMsg.setMsgid(RandomUtil.randomString(16));
+		corpKfSatisfactionMsg.setTouser(externalUserId);
+		corpKfSatisfactionMsg.setOpen_kfid(openKfId);
+		corpKfSatisfactionMsg.setCode(msgCode);
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(corpKfSatisfactionMsg), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("满意度调查内容: " + res.body());
+		}
+
+		CorpKfSessionResp resp = Jsons.parseObject(res.body(), CorpKfSessionResp.class);
+		if (resp.getErrcode() != 0) {
+			throw BusinessRuntimeException.getInstance("满意度调查内容错误: " + resp.getErrmsg());
+		}
+		log.info("发送满意度调查返回:{}", resp.getErrmsg());
+	}
+
 	@Override
 	public CorpKfUserInfo getCorpKfUserInfo(String externalUserid) throws Exception {
 		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/customer/batchget?access_token=" + getAccessToken(yhlXWxCorpConfig.getCorpId());
@@ -850,4 +1043,150 @@ public class WxCorpOpsImpl implements WxCorpOps {
 		List<CorpKfUserInfo> customerList = re.getJSONArray("customer_list").toList(CorpKfUserInfo.class);
 		return customerList.isEmpty() ? null : customerList.get(0);
 	}
+
+	@Override
+	public String getCorpKfUrl(String openKfId, String scene) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/add_contact_way?access_token=" + getAccessToken(yhlXWxCorpConfig.getCorpId());
+		JSONObject params = new JSONObject();
+		params.putOpt("open_kfid", openKfId);
+		if (StrUtil.isNotEmpty(scene)) {
+			params.putOpt("scene", scene);
+		}
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(params), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("微信客服账号链接信息: " + res.body());
+		}
+		JSONObject re = Jsons.parseObject(res.body(), JSONObject.class);
+		if (re.getInt("errcode") != 0) {
+			throw BusinessRuntimeException.getInstance("微信客服账号链接信息错误: " + re.getStr("errmsg"));
+		}
+		return re.getStr("url");
+	}
+
+	@Override
+	public void delCorpKfServicer(String openKfId, List<String> delDutyFollowId) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/servicer/del?access_token=" + getAccessToken(yhlXWxCorpConfig.getCorpId());
+		JSONObject params = new JSONObject();
+		params.putOpt("open_kfid", openKfId);
+		params.putOpt("userid_list", delDutyFollowId);
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(params), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("删除接待人员信息: " + res.body());
+		}
+		JSONObject re = Jsons.parseObject(res.body(), JSONObject.class);
+		if (re.getInt("errcode") != 0) {
+			throw BusinessRuntimeException.getInstance("删除接待人员信息错误: " + re.getStr("errmsg"));
+		}
+	}
+
+	@Override
+	public void addCorpKfServicer(String openKfId, Set<String> dutyFollowIds) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/servicer/add?access_token=" + getAccessToken(yhlXWxCorpConfig.getCorpId());
+		JSONObject params = new JSONObject();
+		params.putOpt("open_kfid", openKfId);
+		params.putOpt("userid_list", dutyFollowIds);
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(params), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("添加接待人员信息: " + res.body());
+		}
+		JSONObject re = Jsons.parseObject(res.body(), JSONObject.class);
+		if (re.getInt("errcode") != 0) {
+			throw BusinessRuntimeException.getInstance("添加接待人员信息错误: " + re.getStr("errmsg"));
+		}
+	}
+
+	@Override
+	public void sendTextMsg(String corpId, WxCorpKfMsgSendRequest request) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/send_msg?access_token=" + getAccessToken(corpId);
+
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(request), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("发送文本事件响应消息: " + res.body());
+		}
+
+		WxCpKfMsgSendResp resp = Jsons.parseObject(res.body(), WxCpKfMsgSendResp.class);
+		if (resp.getErrcode() != 0) {
+			throw BusinessRuntimeException.getInstance("发送文本事件响应消息错误: " + resp.getErrmsg());
+		}
+	}
+
+
+	@Override
+	public ServiceStateResponse getCorpKfSessionStatus(String corpId, String openKfId, String externalUserid) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/kf/service_state/get?access_token=" + getAccessToken(corpId);
+		JSONObject jsonObject = new JSONObject();
+		jsonObject.putOpt("open_kfid", openKfId);
+		jsonObject.putOpt("external_userid", externalUserid);
+		HttpResponse<String> res =
+				J11HttpC.custom()
+						.ofPost()
+						.url(url)
+						.headers(J11HttpC.ReqType.raw_json)
+						.body(HttpRequest.BodyPublishers.ofString(Jsons.toJson(jsonObject), IoKit.Charsets.UTF_8.getCharset()))
+						.send(HttpResponse.BodyHandlers.ofString());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("用户会话状态消息: " + res.body());
+		}
+		ServiceStateResponse serviceStateResponse = Jsons.parseObject(res.body(), ServiceStateResponse.class);
+		if (serviceStateResponse.getErrcode() != 0) {
+			throw BusinessRuntimeException.getInstance("添加接待人员信息错误: " + serviceStateResponse.getErrmsg());
+		}
+		return serviceStateResponse;
+	}
+
+	@Override
+	public String getMedia(String corpId, String mediaId) throws Exception {
+		if (StringUtils.isBlank(mediaId)){
+			return null;
+		}
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token=" + getAccessToken(corpId) + "&media_id=" + mediaId;
+		HttpResponse<byte []> res =
+				J11HttpC.custom()
+						.ofGet()
+						.url(url)
+						.send(HttpResponse.BodyHandlers.ofByteArray());
+		if (200 != res.statusCode()) {
+			throw BusinessRuntimeException.getInstance("企业微信获取素材错误:" + res.body());
+		}
+		// 提取文件名
+		String contentDisposition = res.headers().firstValue("Content-Disposition").orElse("");
+		String fileName = null;
+		if (contentDisposition.contains("filename=")) {
+			fileName = contentDisposition.substring(contentDisposition.indexOf("filename=") + 9).replace("\"", "");
+		}
+		byte[] body = res.body();
+		Path tempFile = Files.createTempFile("media" + System.currentTimeMillis(), StrUtil.subAfter(fileName, StrUtil.DOT, true));
+		try (FileImageOutputStream imageOutput = new FileImageOutputStream(tempFile.toFile())) {
+			imageOutput.write(body, 0, body.length);
+		}
+		Map<String, Object> paramMap = new HashMap<>();
+		paramMap.put("file", tempFile.toFile());
+		JSONObject result = JSONUtil.parseObj(HttpUtil.post("https://files.liuliangbang.vip/pic/ups", paramMap));
+		FileUtil.del(tempFile);
+		return result.getJSONObject("value").getJSONArray("saved").getJSONObject(0).getJSONObject("info").getStr("cdnUrl");
+	}
 }

+ 36 - 0
netflix-service/src/main/java/com/cyksj/service/corp/kf/CorpKfService.java

@@ -0,0 +1,36 @@
+package com.cyksj.service.corp.kf;
+
+import com.cyksj.model.entity.CorpKfAccount;
+import com.cyksj.model.entity.CorpKfAccountSessionConfig;
+import com.cyksj.model.request.corp.kf.CorpKfDutyReq;
+import com.cyksj.model.views.CorpKfDutyServicersStatusView;
+
+import java.util.List;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfService
+ * 创建者: JavaZou
+ * 创建时间:2024/12/30 18:10
+ */
+public interface CorpKfService {
+	void addKfAccount(CorpKfAccount corpKfAccount) throws Exception;
+
+	void delKfAccount(Long id) throws Exception;
+
+	void updateKfAccount(CorpKfAccount corpKfAccount) throws Exception;
+
+	void setKfAccountRuleOrAutoReply(CorpKfAccountSessionConfig CorpKfAccountSessionConfig);
+
+	void setKfDuty(CorpKfDutyReq dutyReq) throws Exception;
+
+	void servicerCommute(long cmsUserId, String openKfId, Boolean status) throws Exception;
+
+	List<CorpKfDutyServicersStatusView> getDutyFollowByOpenKfId(long cmsUserId, String openKfId) throws Exception;
+
+	String getUrl(String openKfId, String scene) throws Exception;
+
+	void transCustomer(String followId, String toFollowId, Integer transNum, List<String> transUserIds) throws Exception;
+
+	List<CorpKfAccount> getCorpKfAccountByUId(long cmsUserId);
+}

+ 380 - 0
netflix-service/src/main/java/com/cyksj/service/corp/kf/impl/CorpKfServiceImpl.java

@@ -0,0 +1,380 @@
+package com.cyksj.service.corp.kf.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.exception.BusinessRuntimeException;
+import com.cyksj.common.util.Jsons;
+import com.cyksj.config.corp.YHLXWxCorpConfig;
+import com.cyksj.mapper.corp.CorpFollowMapper;
+import com.cyksj.mapper.corp.kf.*;
+import com.cyksj.mapper.manage.cms.CmsUserMapper;
+import com.cyksj.model.entity.*;
+import com.cyksj.model.kf.*;
+import com.cyksj.model.request.corp.kf.CorpKfDutyReq;
+import com.cyksj.model.views.CorpKfDutyServicersStatusView;
+import com.cyksj.service.corp.WxCorpOps;
+import com.cyksj.service.corp.kf.CorpKfService;
+import com.cyksj.service.mange.cms.service.CmsUserService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfServiceImpl
+ * 创建者: JavaZou
+ * 创建时间:2024/12/30 18:10
+ */
+@Service
+@RequiredArgsConstructor
+public class CorpKfServiceImpl implements CorpKfService {
+	private final CorpKfAccountMapper corpKfAccountMapper;
+
+	private final WxCorpOps wxCorpOps;
+
+	private final YHLXWxCorpConfig yhlxWxCorpConfig;
+
+	private final CorpKfAccountFollowMapper corpKfAccountFollowMapper;
+
+	private final CorpKfAccountSessionConfigMapper corpKfAccountSessionConfigMapper;
+
+	private final CmsUserService cmsUserService;
+
+	private final CorpFollowMapper corpFollowMapper;
+
+	private final CorpKfServicerSessionRecordMapper corpKfServicerSessionRecordMapper;
+
+	private final CmsUserMapper cmsUserMapper;
+
+	@Override
+	public void addKfAccount(CorpKfAccount corpKfAccount) throws Exception {
+		WxCpKfAccountAdd wxCpKfAccountAdd = new WxCpKfAccountAdd();
+		wxCpKfAccountAdd.setName(corpKfAccount.getName());
+		wxCpKfAccountAdd.setMediaId(wxCorpOps.getMediaIdByFileName("image", corpKfAccount.getImg(), yhlxWxCorpConfig.getCorpId()));
+		WxCpKfAccountAddResp resp = wxCorpOps.addAccount(yhlxWxCorpConfig.getCorpId(), wxCpKfAccountAdd);
+		corpKfAccount.setOpenKfId(resp.getOpenKfid());
+		corpKfAccountMapper.insert(corpKfAccount);
+	}
+
+	@Override
+	public void delKfAccount(Long id) throws Exception {
+		CorpKfAccount corpKfAccount = corpKfAccountMapper.selectById(id);
+		WxCpKfAccountDel wxCpKfAccountDel = new WxCpKfAccountDel();
+		wxCpKfAccountDel.setOpenKfid(corpKfAccount.getOpenKfId());
+		wxCorpOps.delAccount(yhlxWxCorpConfig.getCorpId(),wxCpKfAccountDel);
+		corpKfAccountMapper.deleteById(id);
+		//删除人员
+		corpKfAccountFollowMapper.delete(Wrappers.lambdaQuery(CorpKfAccountFollow.class).eq(CorpKfAccountFollow::getOpenKfId, corpKfAccount.getOpenKfId()));
+		//删除session设置
+		corpKfAccountSessionConfigMapper.delete(Wrappers.lambdaQuery(CorpKfAccountSessionConfig.class).eq(CorpKfAccountSessionConfig::getOpenKfId, corpKfAccount.getOpenKfId()));
+	}
+
+	@Override
+	public void updateKfAccount(CorpKfAccount corpKfAccount) throws Exception {
+		if(StringUtils.isNotBlank(corpKfAccount.getName()) && StringUtils.isNotBlank(corpKfAccount.getImg())){
+			CorpKfAccount kfAccount = corpKfAccountMapper.selectById(corpKfAccount.getId());
+			WxCpKfAccountUpd wxCpKfAccountUpd = new WxCpKfAccountUpd();
+			wxCpKfAccountUpd.setOpenKfid(kfAccount.getOpenKfId());
+			if (StringUtils.isNotBlank(corpKfAccount.getName())) {
+				wxCpKfAccountUpd.setName(corpKfAccount.getName());
+			}
+			if (StringUtils.isNotBlank(corpKfAccount.getImg())) {
+				wxCpKfAccountUpd.setMediaId(wxCorpOps.getMediaIdByFileName("image", corpKfAccount.getImg(), yhlxWxCorpConfig.getCorpId()));
+			}
+			wxCorpOps.updAccount(yhlxWxCorpConfig.getCorpId(),wxCpKfAccountUpd);
+		}
+		corpKfAccountMapper.updateById(corpKfAccount);
+	}
+
+	@Override
+	public void setKfAccountRuleOrAutoReply(CorpKfAccountSessionConfig corpKfAccountSessionConfig) {
+		CorpKfAccountSessionConfig db = corpKfAccountSessionConfigMapper.selectOne(Wrappers.lambdaQuery(CorpKfAccountSessionConfig.class)
+				.eq(CorpKfAccountSessionConfig::getOpenKfId, corpKfAccountSessionConfig.getOpenKfId())
+				.last("limit 1"));
+		if (db != null) {
+			corpKfAccountSessionConfig.setId(db.getId());
+		}
+		Long id = corpKfAccountSessionConfig.getId();
+		if (id == null) {
+			corpKfAccountSessionConfigMapper.insert(corpKfAccountSessionConfig);
+			return;
+		}
+		corpKfAccountSessionConfigMapper.updateById(corpKfAccountSessionConfig);
+	}
+
+	@Override
+	public void setKfDuty(CorpKfDutyReq dutyReq) throws Exception {
+		String openKfId = dutyReq.getOpenKfId();
+		List<CorpKfAccountFollow> dutyList = Optional.ofNullable(dutyReq.getDutyList()).orElse(new ArrayList<>());
+		//非单个 删除其他设置
+		if (!dutyReq.getIsUpdateSingle()) {
+			List<Long> exists = dutyList.stream().filter(c -> c.getId() != null).map(CorpKfAccountFollow::getId).collect(Collectors.toList());
+			corpKfAccountFollowMapper.delete(Wrappers.lambdaQuery(CorpKfAccountFollow.class)
+					.eq(CorpKfAccountFollow::getOpenKfId, openKfId)
+					.notIn(!exists.isEmpty(), CorpKfAccountFollow::getId, exists));
+		}
+
+		dutyList.forEach(duty -> {
+			if (duty.getId() != null) {
+				corpKfAccountFollowMapper.updateById(duty);
+				return;
+			}
+			duty.setOpenKfId(openKfId);
+			corpKfAccountFollowMapper.insert(duty);
+		});
+
+		//需要删除的值班
+		if (CollUtil.isNotEmpty(dutyReq.getDelIds())) {
+			corpKfAccountFollowMapper.deleteBatchIds(dutyReq.getDelIds());
+		}
+
+		//将账号下所有值班人员重新设置
+		List<CorpKfAccountFollow> corpKfAccountFollows = corpKfAccountFollowMapper.selectList(Wrappers.lambdaQuery(CorpKfAccountFollow.class)
+				.eq(CorpKfAccountFollow::getOpenKfId, openKfId));
+		//无需值班
+		if (corpKfAccountFollows.isEmpty()) {
+			return;
+		}
+		//值班的客服
+		Set<String> thisDutyFollows = new HashSet<>();
+		for (CorpKfAccountFollow corpKfAccountFollow : corpKfAccountFollows) {
+			thisDutyFollows.addAll(Jsons.parseList(corpKfAccountFollow.getUserIdList(), String.class));
+		}
+		//接待人员列表
+		CorpKfServicerResp corpKfServicerResp = wxCorpOps.getKfServicerList(yhlxWxCorpConfig.getCorpId(), openKfId);
+		List<CorpKfServicerResp.CorpKfServicer> servicerList = corpKfServicerResp.getServicerList();
+		List<String> delDutyFollowIds = new ArrayList<>();
+		for (CorpKfServicerResp.CorpKfServicer corpKfServicer : servicerList) {
+			String userid = corpKfServicer.getUserid();
+			if (userid != null) {
+				if (!thisDutyFollows.contains(userid)) {
+					delDutyFollowIds.add(userid);
+				}
+			}
+		}
+		if (thisDutyFollows.size() > 0) {
+			wxCorpOps.addCorpKfServicer(openKfId, thisDutyFollows);
+		}
+
+		if (delDutyFollowIds.size() > 0) {
+			wxCorpOps.delCorpKfServicer(openKfId, delDutyFollowIds);
+		}
+	}
+
+	@Override
+	public void servicerCommute(long cmsUserId, String openKfId, Boolean status) throws Exception {
+		CorpKfServicerResp corpKfServicerResp = wxCorpOps.getKfServicerList(yhlxWxCorpConfig.getCorpId(), openKfId);
+		List<String> followIds = corpFollowMapper.selectList(Wrappers.lambdaQuery(CorpFollow.class)
+				.eq(CorpFollow::getCmsUserId, cmsUserId)).stream().map(CorpFollow::getUserid).collect(Collectors.toList());
+		if (followIds.isEmpty()) {
+			throw BusinessRuntimeException.getInstance("未关联企业微信成员");
+		}
+		//接待人员列表
+		List<CorpKfServicerResp.CorpKfServicer> servicerList = corpKfServicerResp.getServicerList();
+		List<String> delDutyFollowId = new ArrayList<>();
+		//下班
+		if (!status) {
+			for (CorpKfServicerResp.CorpKfServicer servicer : servicerList) {
+				if (servicer.getUserid() != null) {
+					if (followIds.contains(servicer.getUserid())) {
+						delDutyFollowId.add(servicer.getUserid());
+					}
+				}
+			}
+			if (!delDutyFollowId.isEmpty()) {
+				wxCorpOps.delCorpKfServicer(openKfId, delDutyFollowId);
+			}
+			return;
+		}
+		//上班
+		DateTime now = DateTime.now();
+		String thisHourDate = DateUtil.format(now, "HH:mm:ss");
+		//星期天为1
+		int dayOfWeek = DateUtil.dayOfWeek(now);
+		//是否值班期
+		List<CorpKfAccountFollow> corpKfAccountFollows = corpKfAccountFollowMapper.selectList(Wrappers.lambdaQuery(CorpKfAccountFollow.class)
+				.eq(CorpKfAccountFollow::getOpenKfId, openKfId)
+				.le(CorpKfAccountFollow::getStartTime, thisHourDate)
+				.ge(CorpKfAccountFollow::getEndTime, thisHourDate)
+				.eq(CorpKfAccountFollow::getDutyWeek, dayOfWeek));
+		//无需值班
+		if (corpKfAccountFollows.isEmpty()) {
+			throw BusinessRuntimeException.getInstance("非值班时间");
+		}
+		//目前时间段需要值班的客服
+		Set<String> thisHourDutyFollows = new HashSet<>();
+		for (CorpKfAccountFollow corpKfAccountFollow : corpKfAccountFollows) {
+			thisHourDutyFollows.addAll(Jsons.parseList(corpKfAccountFollow.getUserIdList(), String.class));
+		}
+		String dutyFollowId = null;
+		for (String followId : followIds) {
+			if (thisHourDutyFollows.contains(followId)) {
+				dutyFollowId = followId;
+				break;
+			}
+		}
+		if (dutyFollowId == null) {
+			throw BusinessRuntimeException.getInstance("无关联企业成员");
+		}
+		Set<String> dutyFollowIds = new HashSet<>();
+		for (CorpKfServicerResp.CorpKfServicer corpKfServicer : servicerList) {
+			String userid = corpKfServicer.getUserid();
+			if (userid != null) {
+				dutyFollowIds.add(userid);
+			}
+		}
+		dutyFollowIds.add(dutyFollowId);
+		wxCorpOps.addCorpKfServicer(openKfId, dutyFollowIds);
+	}
+
+	@Override
+	public List<CorpKfDutyServicersStatusView> getDutyFollowByOpenKfId(long cmsUserId, String openKfId) throws Exception {
+		Boolean isRoot = cmsUserService.isRootRole(cmsUserId);
+		//目前时间段需要值班的客服
+		Set<String> dutyFollowIds = getThisHourDutyFollowIds(openKfId);
+		if (dutyFollowIds.isEmpty()) {
+			return new ArrayList<>();
+		}
+		CorpKfServicerResp corpKfServicerResp = wxCorpOps.getKfServicerList(yhlxWxCorpConfig.getCorpId(), openKfId);
+		List<CorpKfServicerResp.CorpKfServicer> servicerList = corpKfServicerResp.getServicerList();
+		//目前值班客服id
+		List<String> corpKfDutyServiceIds = new ArrayList<>();
+		for (CorpKfServicerResp.CorpKfServicer servicer : servicerList) {
+			if (servicer.getUserid() != null && servicer.getStatus() == 0) {
+				corpKfDutyServiceIds.add(servicer.getUserid());
+			}
+		}
+		List<CorpKfDutyServicersStatusView> views = new ArrayList<>();
+		CorpKfDutyServicersStatusView corpKfDutyServicersStatusView = null;
+		//查看目前时间段值班人员情况
+		for (String thisHourDutyFollow : dutyFollowIds) {
+			LambdaQueryWrapper<CorpFollow> wrapper = Wrappers.lambdaQuery(CorpFollow.class)
+					.eq(CorpFollow::getUserid, thisHourDutyFollow);
+			//非root 只查询当前用户当前时间段值班情况
+			if (!isRoot) {
+				wrapper.eq(CorpFollow::getCmsUserId, cmsUserId);
+			}
+			CorpFollow corpFollow = corpFollowMapper.selectOne(wrapper
+					.last("limit 1"));
+			if (corpFollow == null) {
+				continue;
+			}
+			corpKfDutyServicersStatusView = new CorpKfDutyServicersStatusView();
+			Long relateUserId = corpFollow.getCmsUserId();
+			CmsUser cmsUser = cmsUserMapper.selectById(relateUserId);
+			if (cmsUser != null) {
+				corpKfDutyServicersStatusView.setName(cmsUser.getNickname());
+			}
+			corpKfDutyServicersStatusView.setFollowName(corpFollow.getName());
+			corpKfDutyServicersStatusView.setCmsUserId(corpFollow.getCmsUserId());
+			if (!corpKfDutyServiceIds.contains(thisHourDutyFollow)) {
+				//未值班
+				corpKfDutyServicersStatusView.setStatus(0);
+			} else {
+				corpKfDutyServicersStatusView.setStatus(1);
+			}
+			views.add(corpKfDutyServicersStatusView);
+		}
+		return views;
+	}
+
+	@Override
+	public String getUrl(String openKfId, String scene) throws Exception {
+		return wxCorpOps.getCorpKfUrl(openKfId, scene);
+	}
+
+	/**
+	 * 转接客服
+	 */
+	@Override
+	public void transCustomer(String followId, String toFollowId, Integer transNum, List<String> transUserIds) throws Exception {
+		List<CorpKfAccountFollow> corpKfAccountFollows = corpKfAccountFollowMapper.selectList(Wrappers.lambdaQuery(CorpKfAccountFollow.class)
+				.apply(String.format("JSON_CONTAINS(user_id_list,'\"%s\"')", followId)));
+		List<String> openKfIds = new ArrayList<>();
+		for (CorpKfAccountFollow corpKfAccountFollow : corpKfAccountFollows) {
+			String userIdList = corpKfAccountFollow.getUserIdList();
+			List<String> followIds = Jsons.parseList(userIdList, String.class);
+			followIds.remove(followIds);
+			followIds.add(toFollowId);
+			corpKfAccountFollowMapper.updateById(corpKfAccountFollow);
+			if (!openKfIds.contains(corpKfAccountFollow.getOpenKfId())) {
+				openKfIds.add(corpKfAccountFollow.getOpenKfId());
+			}
+		}
+		List<CorpKfServicerSessionRecord> corpKfServicerSessionRecords = corpKfServicerSessionRecordMapper.selectList(Wrappers.lambdaQuery(CorpKfServicerSessionRecord.class)
+				.eq(CorpKfServicerSessionRecord::getServiceState, 0)
+				.apply(transNum != null, String.format("order by rand() limit %s", transNum))
+				.in(CollUtil.isNotEmpty(transUserIds), CorpKfServicerSessionRecord::getExternalUserid, transUserIds));
+		String corpId = yhlxWxCorpConfig.getCorpId();
+		//添加对应客服
+		for (String openKfId : openKfIds) {
+			CorpKfServicerResp corpKfServicerResp = wxCorpOps.getKfServicerList(yhlxWxCorpConfig.getCorpId(), openKfId);
+			List<CorpKfServicerResp.CorpKfServicer> servicerList = corpKfServicerResp.getServicerList();
+			Set<String> dutyFollowIds = new HashSet<>();
+			for (CorpKfServicerResp.CorpKfServicer corpKfServicer : servicerList) {
+				String userid = corpKfServicer.getUserid();
+				if (userid != null) {
+					dutyFollowIds.add(userid);
+				}
+			}
+			if (!dutyFollowIds.isEmpty()) {
+				dutyFollowIds.remove(followId);
+				dutyFollowIds.add(toFollowId);
+				wxCorpOps.addCorpKfServicer(openKfId, dutyFollowIds);
+			}
+		}
+		for (CorpKfServicerSessionRecord corpKfServicerSessionRecord : corpKfServicerSessionRecords) {
+			//调用微信客服接口 变更会话状态
+			wxCorpOps.transCorpKfSessionToServicer(corpId, corpKfServicerSessionRecord.getOpenKfId(), toFollowId, corpKfServicerSessionRecord.getExternalUserid(), 3);
+		}
+	}
+
+	@Override
+	public List<CorpKfAccount> getCorpKfAccountByUId(long cmsUserId) {
+		List<CorpKfAccount> corpKfAccounts = corpKfAccountMapper.selectList(null);
+		List<String> followIds = corpFollowMapper.selectList(Wrappers.lambdaQuery(CorpFollow.class)
+				.eq(CorpFollow::getCmsUserId, cmsUserId)).stream().map(CorpFollow::getUserid).collect(Collectors.toList());
+
+		return corpKfAccounts.stream().filter(account -> {
+			String openKfId = account.getOpenKfId();
+			for (String followId : followIds) {
+				Integer selectCount = corpKfAccountFollowMapper.selectCount(Wrappers.lambdaQuery(CorpKfAccountFollow.class)
+						.eq(CorpKfAccountFollow::getOpenKfId, openKfId)
+						.apply(String.format("JSON_CONTAINS(user_id_list,'\"%s\"')", followId)));
+				if (selectCount > 0) {
+					return true;
+				}
+			}
+			return false;
+		}).collect(Collectors.toList());
+	}
+
+	/**
+	 * 获取当前值班客服
+	 */
+	public Set<String> getThisHourDutyFollowIds(String openKfId) throws Exception {
+		DateTime now = DateTime.now();
+		String thisHourDate = DateUtil.format(now, "HH:mm");
+		//星期天为1
+		int dayOfWeek = DateUtil.dayOfWeek(now);
+		//是否值班期
+		List<CorpKfAccountFollow> corpKfAccountFollows = corpKfAccountFollowMapper.selectList(Wrappers.lambdaQuery(CorpKfAccountFollow.class)
+				.eq(CorpKfAccountFollow::getOpenKfId, openKfId)
+				.le(CorpKfAccountFollow::getStartTime, thisHourDate)
+				.ge(CorpKfAccountFollow::getEndTime, thisHourDate)
+				.eq(CorpKfAccountFollow::getDutyWeek, dayOfWeek));
+
+		//目前时间段需要值班的客服
+		Set<String> thisHourDutyFollows = new HashSet<>();
+		for (CorpKfAccountFollow corpKfAccountFollow : corpKfAccountFollows) {
+			thisHourDutyFollows.addAll(Jsons.parseList(corpKfAccountFollow.getUserIdList(), String.class));
+		}
+		return thisHourDutyFollows;
+	}
+}

+ 0 - 1
netflix-service/src/main/java/com/cyksj/service/order/impl/OrderCommonServiceImpl.java

@@ -44,7 +44,6 @@ public class OrderCommonServiceImpl implements OrderCommonService {
 	private final RedisService redisService;
 
 	private final GroupRelationClearService clearService;
-
 	@Override
 	public void clearUnrealGoodsDetail(OrderDon orderDon, Integer goodsType, GoodsDonSku sku) {
 		if (goodsType == 1 && sku != null) {

+ 2 - 0
netflix-service/src/main/java/com/cyksj/service/sys/SysCorpFollowService.java

@@ -14,4 +14,6 @@ public interface SysCorpFollowService extends IService<SysCorpFollow> {
 	void saveOrUpdateFollow(SysCorpFollowReq req) throws Exception;
 
 	void delBySid(Long sid) throws Exception;
+
+	void bindLoginAccount(SysCorpFollow sysCorpFollow);
 }

+ 23 - 0
netflix-service/src/main/java/com/cyksj/service/sys/impl/SysCorpFollowServiceImpl.java

@@ -22,6 +22,7 @@ import org.springframework.stereotype.Service;
 
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
@@ -78,6 +79,8 @@ public class SysCorpFollowServiceImpl extends ServiceImpl<SysCorpFollowMapper, S
 		}
 		//同步企业客服key
 		syncSysCorpUserKey();
+		//绑定银河登录账号
+		bindLoginAccount(sysCorpFollow);
 	}
 
 	private void syncSysCorpUserKey() {
@@ -152,6 +155,26 @@ public class SysCorpFollowServiceImpl extends ServiceImpl<SysCorpFollowMapper, S
 		sysCorpFollowRelateMapper.updateById(sysCorpFollowRelate);
 	}
 
+	@Override
+	public void bindLoginAccount(SysCorpFollow sysCorpFollow) {
+		Long id = sysCorpFollow.getId();
+		SysCorpFollow dbSysFollow = getById(id);
+		Long cmsUserId = sysCorpFollow.getCmsUserId();
+		if (cmsUserId != null) {
+			dbSysFollow.setCmsUserId(cmsUserId);
+			List<SysCorpFollowRelate> sysCorpFollowRelates = sysCorpFollowRelateMapper.selectList(Wrappers.lambdaQuery(SysCorpFollowRelate.class)
+					.eq(SysCorpFollowRelate::getMid, id));
+			for (SysCorpFollowRelate sysCorpFollowRelate : sysCorpFollowRelates) {
+				Optional.ofNullable(corpFollowMapper.selectOne(Wrappers.lambdaQuery(CorpFollow.class)
+						.eq(CorpFollow::getUserid, sysCorpFollowRelate.getFollowId())
+						.last("limit 1"))).ifPresent(follow -> {
+					follow.setCmsUserId(dbSysFollow.getCmsUserId());
+					corpFollowMapper.updateById(follow);
+				});
+			}
+		}
+	}
+
 	public void updateCorpFollowRelateConfig(SysCorpFollowRelate sysCorpFollowRelate, SysCorpFollowRelate db) {
 		Long sid = sysCorpFollowRelate.getId();
 		if (!StrUtil.equals(db.getFollowId(), sysCorpFollowRelate.getFollowId())) {

+ 50 - 0
netflix-web/src/main/java/com/cyksj/web/controller/kf/CorpKfAppletController.java

@@ -0,0 +1,50 @@
+package com.cyksj.web.controller.kf;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.mapper.corp.kf.CorpKfAccountMapper;
+import com.cyksj.model.entity.CorpKfAccount;
+import com.cyksj.redis.RedisService;
+import com.cyksj.service.corp.WxCorpOps;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfController
+ * 创建者: JavaZou
+ * 创建时间:2025/1/10 11:18
+ */
+@RestController
+@RequestMapping("/applets/corp/kf")
+@RequiredArgsConstructor
+public class CorpKfAppletController {
+	private final CorpKfAccountMapper corpKfAccountMapper;
+
+	private final WxCorpOps wxCorpOps;
+
+	private final RedisService redisService;
+
+	/**
+	 * 获取微信客服
+	 */
+	@GetMapping("/get")
+	public Result<String> getCorpKf(Integer scene) throws Exception {
+		CorpKfAccount corpKfAccount = corpKfAccountMapper.selectOne(Wrappers.lambdaQuery(CorpKfAccount.class).eq(CorpKfAccount::getScene, scene).last("limit 1"));
+		if (corpKfAccount == null) {
+			return GatewayResponse.SUCCESS.newBuilder().toResult();
+		}
+		String openKfId = corpKfAccount.getOpenKfId();
+		String key = RedisService.key.CORP_KF_ACCOUNT_URL.getName() + openKfId;
+		String url = redisService.getStr(key);
+		if (StrUtil.isEmpty(url)) {
+			url = wxCorpOps.getCorpKfUrl(openKfId, null);
+			redisService.setNx(key, url, RedisService.key.CORP_KF_ACCOUNT_URL.getTimeout());
+		}
+		return GatewayResponse.SUCCESS.newBuilder().toResult(url);
+	}
+}

+ 12 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/CmsCorpFollowActiveCodeController.java

@@ -7,6 +7,7 @@ import com.cyksj.common.util.Jsons;
 import com.cyksj.dto.Result;
 import com.cyksj.enums.GatewayResponse;
 import com.cyksj.mapper.OrderDonMapper;
+import com.cyksj.mapper.corp.CorpFollowMapper;
 import com.cyksj.mapper.corp.CorpUserMapper;
 import com.cyksj.mapper.corp.CorpWelcomeTemplateMapper;
 import com.cyksj.mapper.manage.coupon.CouponMapper;
@@ -65,6 +66,8 @@ public class CmsCorpFollowActiveCodeController {
 
 	private final HttpServletRequest request;
 
+	private final CorpFollowMapper corpFollowMapper;
+
 	/**
 	 * 获取企业成员列表
 	 */
@@ -76,6 +79,15 @@ public class CmsCorpFollowActiveCodeController {
 		return GatewayResponse.SUCCESS.newBuilder().toResult(search);
 	}
 
+	/**
+	 * 编辑企业成员
+	 */
+	@PutMapping("/update/follow")
+	public Result<String> updateFollow(@RequestBody CorpFollow corpFollow) {
+		corpFollowMapper.updateById(corpFollow);
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
 
 	/**
 	 * 生成动态配置

+ 282 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/wxkf/CorpKfController.java

@@ -0,0 +1,282 @@
+package com.cyksj.web.controller.manage.corp.wxkf;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.annotation.Log;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.BusinessType;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.mapper.corp.CorpFollowMapper;
+import com.cyksj.mapper.corp.kf.CorpKfAccountUserChangeRecordMapper;
+import com.cyksj.model.entity.*;
+import com.cyksj.model.request.CorpKfDutySetReq;
+import com.cyksj.model.request.corp.kf.CorpKfDutyReq;
+import com.cyksj.model.views.CorpKfDutyServicersStatusView;
+import com.cyksj.model.views.CorpKfUserChatRecordView;
+import com.cyksj.model.views.SysCorpFollowView;
+import com.cyksj.service.corp.kf.CorpKfService;
+import com.cyksj.service.mange.cms.service.CmsUserService;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.param.Operator;
+import com.ejlchina.searcher.util.MapBuilder;
+import com.ejlchina.searcher.util.MapUtils;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * admin/微信客服
+ * 项目名: yhlxj2
+ * 文件名: CorpWxKfController
+ * 创建者: JavaZou
+ * 创建时间:2024/12/30 18:02
+ */
+@RestController
+@RequestMapping("/manage/corp/kf")
+@RequiredArgsConstructor
+public class CorpKfController {
+	private final CorpKfService corpKfService;
+
+	private final BeanSearcher beanSearcher;
+
+	private final HttpServletRequest request;
+
+	private final CmsUserService cmsUserService;
+
+	private final CorpFollowMapper corpFollowMapper;
+
+	private final CorpKfAccountUserChangeRecordMapper corpKfAccountUserChangeRecordMapper;
+
+
+	/**
+	 * 添加客服账号
+	 */
+	@PostMapping("/account/add")
+	public Result<String> addKfAccount(@RequestBody CorpKfAccount corpKfAccount) throws Exception {
+		corpKfService.addKfAccount(corpKfAccount);
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
+	/**
+	 * 删除客服账号
+	 */
+	@DeleteMapping("/account/del/{id}")
+	public Result<String> delKfAccount(@PathVariable Long id) throws Exception {
+		corpKfService.delKfAccount(id);
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
+	/**
+	 * 修改客服账号
+	 */
+	@PutMapping("/account/update")
+	public Result<String> updateKfAccount(@RequestBody CorpKfAccount corpKfAccount) throws Exception {
+		corpKfService.updateKfAccount(corpKfAccount);
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
+	/**
+	 * 客服账号列表
+	 */
+	@GetMapping("/account/get")
+	public Result<SearchResult<CorpKfAccount>> getKfAccount() {
+		SearchResult<CorpKfAccount> search = beanSearcher.search(CorpKfAccount.class, MapUtils.flatBuilder(request.getParameterMap()).build());
+		return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+	}
+
+	/**
+	 * 获取客服账号链接
+	 */
+	@GetMapping("/get/url")
+	public Result<String> getUrl(String openKfId, String scene) throws Exception {
+		String url = corpKfService.getUrl(openKfId, scene);
+		return GatewayResponse.SUCCESS.newBuilder().toResult(url);
+	}
+
+	//===============================================账号会话设置========================================================================
+
+	/**
+	 * 设置规则
+	 */
+	@PostMapping("/set/rule")
+	public Result<String> setKfAccountRule(@RequestBody CorpKfAccountSessionConfig CorpKfAccountSessionConfig) {
+		corpKfService.setKfAccountRuleOrAutoReply(CorpKfAccountSessionConfig);
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
+	/**
+	 * 获取规则设置
+	 */
+	@GetMapping("/get/rule")
+	public Result<CorpKfAccountSessionConfig> getRule(String openKfId) {
+		CorpKfAccountSessionConfig corpKfAccountSessionConfig = beanSearcher.searchFirst(CorpKfAccountSessionConfig.class, MapUtils.flatBuilder(request.getParameterMap()).field(CorpKfAccountSessionConfig::getOpenKfId, openKfId).build());
+		return GatewayResponse.SUCCESS.newBuilder().toResult(corpKfAccountSessionConfig);
+	}
+
+	/**
+	 * 设置自动回复
+	 */
+	@PostMapping("/set/auto/reply")
+	public Result<String> setKfAccountAutoReply(@RequestBody CorpKfAccountSessionConfig CorpKfAccountSessionConfig) {
+		corpKfService.setKfAccountRuleOrAutoReply(CorpKfAccountSessionConfig);
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
+	/**
+	 * 获取自动回复设置
+	 */
+	@GetMapping("/get/auto/reply")
+	public Result<CorpKfAccountSessionConfig> getAutoReply(String openKfId) {
+		CorpKfAccountSessionConfig corpKfAccountSessionConfig = beanSearcher.searchFirst(CorpKfAccountSessionConfig.class, MapUtils.flatBuilder(request.getParameterMap()).field(CorpKfAccountSessionConfig::getOpenKfId, openKfId).build());
+		return GatewayResponse.SUCCESS.newBuilder().toResult(corpKfAccountSessionConfig);
+	}
+
+
+
+
+	//===============================================账号值班设置========================================================================
+	/**
+	 * 设置值班时间
+	 */
+	@PostMapping("/set/duty")
+	public Result<String> setKfDuty(@RequestBody CorpKfDutyReq dutyReq) throws Exception {
+		corpKfService.setKfDuty(dutyReq);
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
+	/**
+	 * 获取账号值班设置
+	 */
+	@GetMapping("/get/duty")
+	public Result<List<CorpKfAccountFollow>> getKfDuty(String openKfId) {
+		List<CorpKfAccountFollow> corpKfAccountFollows = beanSearcher.searchAll(CorpKfAccountFollow.class, MapUtils.flatBuilder(request.getParameterMap()).field(CorpKfAccountFollow::getOpenKfId, openKfId).build());
+		return GatewayResponse.SUCCESS.newBuilder().toResult(corpKfAccountFollows);
+	}
+
+	//===============================================数据统计========================================================================
+
+	/**
+	 * 数据统计
+	 */
+	@GetMapping("/get/statistics")
+	public Result<SearchResult<CorpKfServicerStatistic>> getStatisticsData() {
+		long adminId = StpUtil.getLoginIdAsLong();
+		Boolean rootRole = cmsUserService.isRootRole(adminId);
+		MapBuilder builder = MapUtils.flatBuilder(request.getParameterMap());
+		if (!rootRole) {
+			List<String> userids = corpFollowMapper.selectList(Wrappers.lambdaQuery(CorpFollow.class)
+					.eq(CorpFollow::getCmsUserId, adminId)).stream().map(CorpFollow::getUserid).collect(Collectors.toList());
+			builder.field(CorpKfServicerStatistic::getServicerUserid, userids).op(Operator.InList);
+		}
+		SearchResult<CorpKfServicerStatistic> search = beanSearcher.search(CorpKfServicerStatistic.class, builder.build());
+		search.getDataList().forEach(data->{
+			SysCorpFollowView sysCorpFollowView = beanSearcher.searchFirst(SysCorpFollowView.class, MapUtils.builder().field(SysCorpFollowView::getFollowId, data.getServicerUserid()).build());
+			if (sysCorpFollowView != null) {
+				data.setName(sysCorpFollowView.getName());
+				data.setFollowName(sysCorpFollowView.getFollowName());
+			}
+		});
+		return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+	}
+
+	//===============================================聊天记录========================================================================
+
+	/**
+	 * 会话记录
+	 */
+	@GetMapping("/get/session")
+	public Result<SearchResult<CorpKfUserChatRecordView>> getSession(String keyword) {
+		MapBuilder builder = MapUtils.flatBuilder(request.getParameterMap());
+		if (StringUtils.isNotBlank(keyword)) {
+			builder.put("keyword", String.format(" and exist (select 1 from corp_kf_account_session_chat ct where ct.session_id = cr.id and ct.msg_content like '%s%%')", keyword));
+		}
+		SearchResult<CorpKfUserChatRecordView> search = beanSearcher.search(CorpKfUserChatRecordView.class, builder.build());
+		return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+	}
+
+
+	/**
+	 * 获取聊天记录
+	 */
+	@GetMapping("/get/session/chat")
+	public Result<SearchResult<CorpKfAccountSessionChat>> getSessionChat(){
+		Map<String, Object> build = MapUtils.flatBuilder(request.getParameterMap()).build();
+		SearchResult<CorpKfAccountSessionChat> search = beanSearcher.search(CorpKfAccountSessionChat.class, build);
+		return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+	}
+
+	/**
+	 * 客服上下班
+	 */
+	@PutMapping("/servicer/commute")
+	@Log(module = "微信客服/客服上下班", businessType = BusinessType.PUT, isSaveRequestData = true)
+	public Result<String> servicerCommute(@RequestBody CorpKfDutySetReq corpKfDutySetReq) throws Exception {
+		long cmsUserId = StpUtil.getLoginIdAsLong();
+		corpKfService.servicerCommute(cmsUserId, corpKfDutySetReq.getOpenKfId(), corpKfDutySetReq.getStatus());
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
+	/**
+	 * 目前值班人员变化
+	 */
+	@GetMapping("/get/duty/{openKfId}")
+	public Result<List<CorpKfDutyServicersStatusView>> getDutyFollowByOpenKfId(@PathVariable String openKfId) throws Exception {
+		long cmsUserId = StpUtil.getLoginIdAsLong();
+		List<CorpKfDutyServicersStatusView> dutyStatuses = corpKfService.getDutyFollowByOpenKfId(cmsUserId, openKfId);
+		return GatewayResponse.SUCCESS.newBuilder().toResult(dutyStatuses);
+	}
+
+	/**
+	 * 目前用户值班微信客服
+	 */
+	@GetMapping("/get/duty/accounts")
+	public Result<List<CorpKfAccount>> getCorpKfAccountByUId() {
+		long cmsUserId = StpUtil.getLoginIdAsLong();
+		List<CorpKfAccount> corpKfAccounts = corpKfService.getCorpKfAccountByUId(cmsUserId);
+		return GatewayResponse.SUCCESS.newBuilder().toResult(corpKfAccounts);
+	}
+
+	/**
+	 * 记录用户切换客服记录
+	 */
+	@PostMapping("/change/account/record/{openKfId}")
+	public Result<String> recordUserAccountRecord(@PathVariable String openKfId) {
+		long cmsUserId = StpUtil.getLoginIdAsLong();
+		CorpKfAccountUserChangeRecord corpKfAccountUserChangeRecord = Optional.ofNullable(corpKfAccountUserChangeRecordMapper.selectOne(Wrappers.lambdaQuery(CorpKfAccountUserChangeRecord.class)
+				.eq(CorpKfAccountUserChangeRecord::getCmsUserId, cmsUserId)
+				.last("limit 1"))).orElse(new CorpKfAccountUserChangeRecord());
+		corpKfAccountUserChangeRecord.setCmsUserId(cmsUserId);
+		corpKfAccountUserChangeRecord.setOpenKfId(openKfId);
+		if (corpKfAccountUserChangeRecord.getId() == null) {
+			try {
+				corpKfAccountUserChangeRecordMapper.insert(corpKfAccountUserChangeRecord);
+			} catch (DuplicateKeyException e) {
+			}
+		} else {
+			corpKfAccountUserChangeRecordMapper.updateById(corpKfAccountUserChangeRecord);
+		}
+
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
+	/**
+	 * 用户切换德微信账号
+	 */
+	@GetMapping("/get/change/account")
+	public Result<CorpKfAccountUserChangeRecord> getChangeAccount() {
+		long cmsUserId = StpUtil.getLoginIdAsLong();
+		CorpKfAccountUserChangeRecord corpKfAccountUserChangeRecord = Optional.ofNullable(corpKfAccountUserChangeRecordMapper.selectOne(Wrappers.lambdaQuery(CorpKfAccountUserChangeRecord.class)
+				.eq(CorpKfAccountUserChangeRecord::getCmsUserId, cmsUserId)
+				.last("limit 1"))).orElse(new CorpKfAccountUserChangeRecord());
+		return GatewayResponse.SUCCESS.newBuilder().toResult(corpKfAccountUserChangeRecord);
+	}
+
+}

+ 86 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/wxkf/CorpKfMsgController.java

@@ -0,0 +1,86 @@
+package com.cyksj.web.controller.manage.corp.wxkf;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.mapper.corp.kf.CorpKfServicerSessionRecordMapper;
+import com.cyksj.mapper.corp.kf.CorpKfUserMapper;
+import com.cyksj.model.entity.CorpKfServicerSessionRecord;
+import com.cyksj.model.entity.CorpKfUser;
+import com.cyksj.model.kf.CorpKfUserInfo;
+import com.cyksj.service.corp.WxCorpOps;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 项目名: yhlxj2
+ * 文件名: CorpKfMsgController
+ * 创建者: JavaZou
+ * 创建时间:2025/1/2 13:59
+ */
+@RestController
+@RequestMapping("/manage/corp/kf")
+@RequiredArgsConstructor
+public class CorpKfMsgController {
+	private final WxCorpOps wxCorpOps;
+
+	private final CorpKfServicerSessionRecordMapper corpKfServicerSessionRecordMapper;
+
+	private final CorpKfUserMapper corpKfUserMapper;
+
+	/**
+	 * 发送满意度调查消息
+	 */
+	@PostMapping("/send/satisfaction/msg")
+	public void sendSatisfactionMsg(Long id) throws Exception {
+		CorpKfServicerSessionRecord corpKfServicerSessionRecord = corpKfServicerSessionRecordMapper.selectById(id);
+		//更新发送满意度字段
+		if (corpKfServicerSessionRecord != null) {
+			corpKfServicerSessionRecord.setSendSatisfaction(true);
+			corpKfServicerSessionRecordMapper.updateById(corpKfServicerSessionRecord);
+			wxCorpOps.sendSatisfactionMsg(corpKfServicerSessionRecord.getMsgid(), corpKfServicerSessionRecord.getOpenKfId(), corpKfServicerSessionRecord.getExternalUserid(), null);
+		}
+	}
+
+	/**
+	 * 获取客户用户信息
+	 */
+	@GetMapping("/get/user/info")
+	public Result<CorpKfUserInfo> getUserInfo(String externalUserid) throws Exception {
+		CorpKfUserInfo corpKfUserInfo = wxCorpOps.getCorpKfUserInfo(externalUserid);
+		CorpKfUser corpKfUser = corpKfUserMapper.selectOne(Wrappers.lambdaQuery(CorpKfUser.class)
+				.eq(CorpKfUser::getExternalUserid, externalUserid)
+				.last("limit 1"));
+		if (corpKfUser == null) {
+			try {
+				corpKfUser = new CorpKfUser();
+				BeanUtil.copyProperties(corpKfUserInfo, corpKfUser);
+				corpKfUserMapper.insert(corpKfUser);
+			} catch (Exception e) {
+			}
+		} else {
+			corpKfUser.setAvatar(corpKfUserInfo.getAvatar());
+			corpKfUser.setNickname(corpKfUserInfo.getNickname());
+			corpKfUser.setGender(corpKfUserInfo.getGender());
+			corpKfUserMapper.updateById(corpKfUser);
+		}
+		return GatewayResponse.SUCCESS.newBuilder().toResult(corpKfUserInfo);
+	}
+
+	/**
+	 * 查询用户会话
+	 */
+	@GetMapping("/get/user/session")
+	public Result<CorpKfServicerSessionRecord> getUserSession(String userid, String externalUserid) {
+		CorpKfServicerSessionRecord corpKfServicerSessionRecord = corpKfServicerSessionRecordMapper.selectOne(Wrappers.lambdaQuery(CorpKfServicerSessionRecord.class)
+				.eq(CorpKfServicerSessionRecord::getServicerUserid, userid)
+				.eq(CorpKfServicerSessionRecord::getExternalUserid, externalUserid)
+				.eq(CorpKfServicerSessionRecord::getServiceState, 0)
+				.last("limit 1"));
+		return GatewayResponse.SUCCESS.newBuilder().toResult(corpKfServicerSessionRecord);
+	}
+}

+ 42 - 0
netflix-web/src/main/java/com/cyksj/web/controller/sys/SysCorpFollowController.java

@@ -4,14 +4,20 @@ import cn.hutool.core.collection.CollUtil;
 import com.cyksj.config.corp.YHLXWxCorpConfig;
 import com.cyksj.dto.Result;
 import com.cyksj.enums.GatewayResponse;
+import com.cyksj.mapper.SysCorpFollowRelateMapper;
 import com.cyksj.mapper.corp.CorpUserFollowRelationMapper;
+import com.cyksj.model.entity.SysCorpFollow;
+import com.cyksj.model.entity.SysCorpFollowRelate;
 import com.cyksj.model.request.sys.SysCorpFollowReq;
+import com.cyksj.model.views.CorpKfTransUserView;
 import com.cyksj.model.views.SysCorpFollowView;
 import com.cyksj.model.views.SysCorpMasterFollowView;
 import com.cyksj.service.corp.CorpService;
 import com.cyksj.service.corp.WxCorpOps;
+import com.cyksj.service.corp.kf.CorpKfService;
 import com.cyksj.service.sys.SysCorpFollowService;
 import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
 import com.ejlchina.searcher.util.MapUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -50,6 +56,10 @@ public class SysCorpFollowController {
 
 	private final YHLXWxCorpConfig corpConfig;
 
+	private final CorpKfService corpKfService;
+
+	private final SysCorpFollowRelateMapper sysCorpFollowRelateMapper;
+
 	/**
 	 * 新增客服
 	 */
@@ -115,4 +125,36 @@ public class SysCorpFollowController {
 		corpId = Optional.ofNullable(corpId).orElse(yhlxWxCorpConfig.getCorpId());
 		return GatewayResponse.SUCCESS.newBuilder().toResult(corpService.dataInitByCorpId(corpId));
 	}
+
+	/**
+	 * 客服绑定登录账号
+	 */
+	@PutMapping("/bind/loginAccount")
+	public Result<String> bindLoginAccount(@RequestBody SysCorpFollow sysCorpFollow) {
+		sysCorpFollowService.bindLoginAccount(sysCorpFollow);
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
+	/**
+	 * 转接客户
+	 */
+	@PostMapping("/tans/customer")
+	public Result<String> transCustomer(@RequestBody SysCorpFollowReq req) throws Exception {
+		String toFollowId = req.getToFollowId();
+		Long sid = req.getSid();
+		SysCorpFollowRelate sysCorpFollowRelate = sysCorpFollowRelateMapper.selectById(sid);
+		corpKfService.transCustomer(sysCorpFollowRelate.getFollowId(), toFollowId, req.getTransNum(), req.getTransUserIds());
+		sysCorpFollowRelate.setToFollowId(toFollowId);
+		sysCorpFollowRelateMapper.updateById(sysCorpFollowRelate);
+		return GatewayResponse.SUCCESS.newBuilder().toResult();
+	}
+
+	/**
+	 * 转接客户列表
+	 */
+	@GetMapping("/get/trans/users")
+	public Result<SearchResult<CorpKfTransUserView>> getTransUsers() {
+		SearchResult<CorpKfTransUserView> search = beanSearcher.search(CorpKfTransUserView.class, MapUtils.flatBuilder(request.getParameterMap()).build());
+		return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+	}
 }