Kaynağa Gözat

企微会话记录

zwhui 2 yıl önce
ebeveyn
işleme
820d8f9295
26 değiştirilmiş dosya ile 981 ekleme ve 3 silme
  1. 2 0
      netflix-common/src/main/java/com/cyksj/dto/RedisKey.java
  2. 11 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/CorpMsgFollowAssociationMapper.java
  3. 11 0
      netflix-dao/src/main/java/com/cyksj/mapper/corp/QyMsgContentMapper.java
  4. 1 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpFollow.java
  5. 23 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpMsgFollowAssociation.java
  6. 6 0
      netflix-dao/src/main/java/com/cyksj/model/entity/CorpUserFollowRelation.java
  7. 125 0
      netflix-dao/src/main/java/com/cyksj/model/entity/QyMsgContent.java
  8. 3 0
      netflix-dao/src/main/java/com/cyksj/model/entity/WxCorpApp.java
  9. 71 0
      netflix-dao/src/main/java/com/cyksj/model/manage/views/CorpFollowMsgView.java
  10. 74 0
      netflix-dao/src/main/java/com/cyksj/model/manage/views/CorpMsgAuditUserView.java
  11. 51 0
      netflix-dao/src/main/java/com/cyksj/model/manage/views/CorpMsgFollowAssociationView.java
  12. 55 0
      netflix-dao/src/main/java/com/cyksj/model/manage/views/CorpQyMsgAuditChatView.java
  13. 80 0
      netflix-dao/src/main/java/com/cyksj/model/manage/views/CorpUserView.java
  14. 11 0
      netflix-service/src/main/java/com/cyksj/service/corp/CorpMsgFollowAssociationService.java
  15. 13 0
      netflix-service/src/main/java/com/cyksj/service/corp/CorpQyMsgContentService.java
  16. 11 0
      netflix-service/src/main/java/com/cyksj/service/corp/WxCorpOps.java
  17. 23 0
      netflix-service/src/main/java/com/cyksj/service/corp/impl/CorpEventActionServiceImpl.java
  18. 15 0
      netflix-service/src/main/java/com/cyksj/service/corp/impl/CorpMsgFollowAssociationServiceImpl.java
  19. 28 0
      netflix-service/src/main/java/com/cyksj/service/corp/impl/CorpQyMsgContentServiceImpl.java
  20. 16 2
      netflix-service/src/main/java/com/cyksj/service/corp/impl/CorpServiceImpl.java
  21. 69 1
      netflix-service/src/main/java/com/cyksj/service/corp/impl/WxCorpOpsImpl.java
  22. 2 0
      netflix-service/src/main/java/com/cyksj/service/corp/msg/CorpEventActionService.java
  23. 99 0
      netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/CorpMsgAuditController.java
  24. 99 0
      netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/CorpMsgFollowController.java
  25. 41 0
      netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/CorpUserController.java
  26. 41 0
      netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/WxCorpController.java

+ 2 - 0
netflix-common/src/main/java/com/cyksj/dto/RedisKey.java

@@ -22,6 +22,8 @@ public class RedisKey {
      */
     public static String CORP_ACCESS_TOKEN_KEY = "corp:access_token:";
 
+    public static String CORP_MSG_ACCESS_TOKEN_KEY = "corp::msg:access_token:";
+
     /**
      * corp_js_ticket
      */

+ 11 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/CorpMsgFollowAssociationMapper.java

@@ -0,0 +1,11 @@
+package com.cyksj.mapper.corp;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.CorpMsgFollowAssociation;
+
+/**
+ * @author chan
+ * @date 2024/1/18 14:22
+ */
+public interface CorpMsgFollowAssociationMapper extends BaseMapper<CorpMsgFollowAssociation> {
+}

+ 11 - 0
netflix-dao/src/main/java/com/cyksj/mapper/corp/QyMsgContentMapper.java

@@ -0,0 +1,11 @@
+package com.cyksj.mapper.corp;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.cyksj.model.entity.QyMsgContent;
+
+/**
+ * @author chan
+ * @date 2024/1/22 15:07
+ */
+public interface QyMsgContentMapper extends BaseMapper<QyMsgContent> {
+}

+ 1 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpFollow.java

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
 import lombok.Data;
 
 import java.io.Serializable;
+import java.util.List;
 
 /**
  * @author chan

+ 23 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpMsgFollowAssociation.java

@@ -0,0 +1,23 @@
+package com.cyksj.model.entity;
+
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Data;
+
+/**
+ * @author chan
+ * @date 2024/1/18 14:00
+ */
+@Data
+@SearchBean(tables = " corp_follow cf,corp_msg_follow_association ca ", where = "cf.userid = ca.follow_id")
+public class CorpMsgFollowAssociation extends BaseEntity{
+
+    @DbField("cf.userid")
+    private String followId;
+
+    @DbField("ca.association_follow_id")
+    private String associationFollowId;
+
+    @DbField("cf.name")
+    private String nickname;
+}

+ 6 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/CorpUserFollowRelation.java

@@ -1,5 +1,6 @@
 package com.cyksj.model.entity;
 
+import com.ejlchina.searcher.bean.DbField;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.Getter;
@@ -74,6 +75,11 @@ public class CorpUserFollowRelation extends BaseEntity implements Serializable {
 	 */
 	private String delUserid;
 
+	/**
+	 * 会话存档同意情况 同意:"Agree",不同意:"Disagree"
+	 */
+	private String agreeStatus;
+
 	@AllArgsConstructor
 	@Getter
 	public enum Way {

+ 125 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/QyMsgContent.java

@@ -0,0 +1,125 @@
+package com.cyksj.model.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+
+@Data
+@NoArgsConstructor
+@Accessors(chain = true)
+@SearchBean(tables = "qy_msg_content")
+public class QyMsgContent {
+
+	@TableId(value = "id",type = IdType.AUTO)
+	private Long id;
+	/**
+	 * 消息id,消息的唯一标识,企业可以使用此字段进行消息去重
+	 */
+	private String msgid;
+	/**
+	 * 消息动作,目前有send(发送消息)/recall(撤回消息)/switch(切换企业日志)三种类型
+	 */
+	private String action;
+
+	/**
+	 * 成员id
+	 */
+	@DbField("follow_id")
+	private String followId;
+
+	/**
+	 * 客户id
+	 */
+	@DbField("external_userid")
+	private String externalUserid;
+
+	/**
+	 * 消息发送方id。同一企业内容为userid,非相同企业为external_userid。消息如果是机器人发出,也为external_userid
+	 */
+	@TableField("`from`")
+	@DbField("`from`")
+	private String from;
+
+	private String fromView;
+	/**
+	 * 消息接收方列表,可能是多个,同一个企业内容为userid,非相同企业为external_userid。数组
+	 */
+	private String tolist;
+
+	private String tolistView;
+	/**
+	 * 群聊消息的群id
+	 */
+	private String roomid;
+
+	private String roomidView;
+	/**
+	 * 消息发送时间戳,utc时间,ms单位
+	 */
+	private String msgtime;
+	/**
+	 * 消息类型
+	 */
+	private String msgtype;
+	/**
+	 * 文字
+	 */
+	private String text;
+	/**
+	 * 图片
+	 */
+	private String image;
+	/**
+	 * 小程序
+	 */
+	private String weapp;
+	/**
+	 * 红包消息
+	 */
+	private String redpacket;
+	/**
+	 * 文件
+	 */
+	private String file;
+	/**
+	 * 视频
+	 */
+	private String video;
+	/**
+	 * 语音
+	 */
+	private String voice;
+	/**
+	 * 多条聊天记录
+	 */
+	private String chatrecord;
+	/**
+	 * 文件名称
+	 */
+	private String filename;
+	/**
+	 * 撤回
+	 */
+	@TableField("`revoke`")
+	@DbField("`revoke`")
+	private String revoke;
+	/**
+	 * 表情包
+	 */
+	private String emotion;
+	/**
+	 * 卡片消息
+	 */
+	private String card;
+	/**
+	 * 文件URL
+	 */
+	private String url;
+
+}

+ 3 - 0
netflix-dao/src/main/java/com/cyksj/model/entity/WxCorpApp.java

@@ -30,6 +30,9 @@ public class WxCorpApp {
     @TableField(value = "corpSecret")
     private String corpSecret;
 
+    @TableField(value = "msgSecret")
+    private String msgSecret;
+
     private String token;
 
     @TableField(value = "encodingAESKey")

+ 71 - 0
netflix-dao/src/main/java/com/cyksj/model/manage/views/CorpFollowMsgView.java

@@ -0,0 +1,71 @@
+package com.cyksj.model.manage.views;
+
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.DbIgnore;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 会话聊天框列表
+ * @author chan
+ * @date 2024/1/22 17:16
+ */
+@Data
+@SearchBean(tables = "(SELECT external_userid userid, q.follow_id follow_id, MAX(id) msgId " +
+        "FROM qy_msg_content q " +
+        " :keyword: " +
+        "GROUP BY q.external_userid , q.follow_id " +
+        "ORDER BY MAX(q.id) desc limit 1) t " +
+        "LEFT JOIN qy_msg_content qm on qm.id = t.msgId " +
+        "LEFT JOIN corp_user_auth cu on cu.external_userid = t.userid " +
+        "LEFT JOIN corp_user c on c.id = cu.corp_user_id "
+)
+public class CorpFollowMsgView {
+
+    /**
+     * 用户名称
+     */
+    @DbField("c.name")
+    private String name;
+
+    /**
+     * 用户头像
+     */
+    @DbField("c.avatar")
+    private String avatar;
+
+    /**
+     * 成员id
+     */
+    @DbField("t.follow_id")
+    private String followId;
+
+    /**
+     * 用户id
+     */
+    @DbField("t.userid")
+    private String userid;
+
+    /**
+     * 最近消息文本内容
+     */
+    @DbField("qm.text")
+    private String msgText;
+
+    /**
+     * 最近消息类型
+     */
+    @DbField("qm.msgtype")
+    private String msgType;
+
+    /**
+     * 最近消息时间
+     */
+    @DbField("qm.msgtime")
+    private String msgtime;
+
+
+}

+ 74 - 0
netflix-dao/src/main/java/com/cyksj/model/manage/views/CorpMsgAuditUserView.java

@@ -0,0 +1,74 @@
+package com.cyksj.model.manage.views;
+
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * @author chan
+ * @date 2024/1/22 14:41
+ */
+
+@Data
+@SearchBean(tables = " corp_user c,corp_user_auth cu, corp_follow f, corp_user_follow_relation cr ,corp_user_tag t ",
+        where = " c.id = cu.corp_user_id " +
+                "and cu.external_userid = cr.corp_user_id " +
+                "and f.userid = cr.follow_id " +
+                ":tag:",
+        groupBy = "c.name,cu.external_userid,c.avatar,c.type,c.gender,f.name,f.userid,cr.agree_status,cr.add_created_time"
+)
+public class CorpMsgAuditUserView {
+
+    /**
+     * 外部联系人的名称
+     */
+    @DbField("c.name")
+    private String name;
+
+    @DbField("cu.external_userid")
+    private String externalUserid;
+
+    /**
+     * 外部联系人头像,代开发自建应用需要管理员授权才可以获取,第三方不可获取,上游企业不可获取下游企业客户该字段
+     */
+    @DbField("c.avatar")
+    private String avatar;
+
+    /**
+     * 外部联系人的类型,1表示该外部联系人是微信用户,2表示该外部联系人是企业微信用户
+     */
+    @DbField("c.type")
+    private Integer type;
+
+    /**
+     * 外部联系人性别 0-未知 1-男性 2-女性。第三方不可获取,上游企业不可获取下游企业客户该字段,返回值为0,表示未定义
+     */
+    @DbField("c.gender")
+    private Integer gender;
+
+    /**
+     * 成员名称
+     */
+    @DbField("f.name")
+    private String followName;
+
+    /**
+     * 成员userid
+     */
+    @DbField("f.userid")
+    private String followId;
+
+    /**
+     * 会话存档同意情况 同意:"Agree",不同意:"Disagree"
+     */
+    @DbField("cr.agree_status")
+    private String agreeStatus;
+
+    /**
+     * 添加时间
+     */
+    @DbField("cr.add_created_time")
+    private Date addCreatedTime;
+}

+ 51 - 0
netflix-dao/src/main/java/com/cyksj/model/manage/views/CorpMsgFollowAssociationView.java

@@ -0,0 +1,51 @@
+package com.cyksj.model.manage.views;
+
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.DbIgnore;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Data;
+import org.aspectj.weaver.ast.Or;
+
+import java.util.List;
+
+/**
+ * @author chan
+ * @date 2024/1/18 14:50
+ */
+@Data
+@SearchBean(tables = " corp_follow c")
+public class CorpMsgFollowAssociationView {
+
+    @DbField("c.wx_corp_id")
+    private Long wxCorpId;
+
+    /**
+     * 成员UserID
+     */
+    @DbField("c.userid")
+    private String userid;
+
+    /**
+     * 成员名称
+     */
+    @DbField("c.name")
+    private String name;
+
+    /**
+     * 头像地址
+     */
+    @DbField("c.avatar")
+    private String avatar;
+
+    /**
+     * 头像缩略图
+     */
+    @DbField("c.thumb_avatar")
+    private String thumbAvatar;
+
+    /**
+     * 关联关系
+     */
+    @DbIgnore
+    private List<CorpMsgFollowAssociationView> associationList;
+}

+ 55 - 0
netflix-dao/src/main/java/com/cyksj/model/manage/views/CorpQyMsgAuditChatView.java

@@ -0,0 +1,55 @@
+package com.cyksj.model.manage.views;
+
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Data;
+
+/**
+ * 企微工具会话列表
+ * @author chan
+ * @date 2024/1/24 13:43
+ */
+@SearchBean(tables = " qy_msg_content q, " +
+        " corp_user c, " +
+        " corp_user_auth cu, " +
+        " corp_follow cf ",
+        where = "q.external_userid = cu.external_userid " +
+                " AND cu.corp_user_id = c.id  " +
+                " AND q.follow_id = cf.userid ",
+        groupBy = "c.name,c.avatar,cu.external_userid ,cf.name,cf.userid,cf.avatar "
+)
+@Data
+public class CorpQyMsgAuditChatView {
+    /**
+     * 外部联系人的名称
+     */
+    @DbField("c.name")
+    private String name;
+
+    /**
+     * 外部联系人头像,代开发自建应用需要管理员授权才可以获取,第三方不可获取,上游企业不可获取下游企业客户该字段
+     */
+    @DbField("c.avatar")
+    private String avatar;
+
+    @DbField("cu.external_userid")
+    private String externalUserid;
+
+    /**
+     * 成员名称
+     */
+    @DbField("cf.name")
+    private String followName;
+
+    /**
+     * 成员userid
+     */
+    @DbField("cf.userid")
+    private String followId;
+
+    /**
+     * 成员头像
+     */
+    @DbField("cf.avatar")
+    private String followAvatar;
+}

+ 80 - 0
netflix-dao/src/main/java/com/cyksj/model/manage/views/CorpUserView.java

@@ -0,0 +1,80 @@
+package com.cyksj.model.manage.views;
+
+import com.ejlchina.searcher.bean.DbField;
+import com.ejlchina.searcher.bean.SearchBean;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author chan
+ * @date 2024/1/19 10:43
+ */
+@Data
+@SearchBean(tables = " corp_user c, corp_user_auth cu ,corp_user_follow_relation cr, corp_user_tag t ",
+        where = "cu.corp_user_id = c.id " +
+                "and cu.external_userid = cr.corp_user_id " +
+                "and cr.id = t.relation_id ",
+        groupBy = "c.name, c.avatar, c.type, c.gender, c.unionid, c.user_id, c.state, c.mark_info, c.created_time"
+)
+public class CorpUserView {
+
+    /**
+     * 外部联系人的名称
+     */
+    @DbField("c.name")
+    private String name;
+
+    /**
+     * 外部联系人头像,代开发自建应用需要管理员授权才可以获取,第三方不可获取,上游企业不可获取下游企业客户该字段
+     */
+    @DbField("c.name")
+    private String avatar;
+
+    /**
+     * 外部联系人的类型,1表示该外部联系人是微信用户,2表示该外部联系人是企业微信用户
+     */
+    @DbField("c.type")
+    private Integer type;
+
+    /**
+     * 外部联系人性别 0-未知 1-男性 2-女性。第三方不可获取,上游企业不可获取下游企业客户该字段,返回值为0,表示未定义
+     */
+    @DbField("c.gender")
+    private Integer gender;
+
+    /**
+     * 外部联系人在微信开放平台的唯一身份标识(微信unionid),通过此字段企业可将外部联系人与公众号/小程序用户关联起来。仅当联系人类型是微信用户,且企业绑定了微信开发者ID有此字段。
+     */
+    @DbField("c.unionid")
+    private String unionid;
+
+    /**
+     * user表 关联用户id
+     */
+    @DbField("c.user_id")
+    private Long userId;
+
+    /**
+     * 添加渠道来源
+     */
+    @DbField("c.state")
+    private String state;
+
+    /**
+     * 客服标记内容
+     */
+    @DbField("c.mark_info")
+    private String markInfo;
+
+    @DbField("c.created_time")
+    private Date createdTime;
+
+
+    /**
+     * 标签组
+     */
+    @DbField("group_concat(t.tag_name)")
+    private List<String> tags;
+}

+ 11 - 0
netflix-service/src/main/java/com/cyksj/service/corp/CorpMsgFollowAssociationService.java

@@ -0,0 +1,11 @@
+package com.cyksj.service.corp;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.cyksj.model.entity.CorpMsgFollowAssociation;
+
+/**
+ * @author chan
+ * @date 2024/1/18 16:16
+ */
+public interface CorpMsgFollowAssociationService extends IService<CorpMsgFollowAssociation> {
+}

+ 13 - 0
netflix-service/src/main/java/com/cyksj/service/corp/CorpQyMsgContentService.java

@@ -0,0 +1,13 @@
+package com.cyksj.service.corp;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.cyksj.model.entity.QyMsgContent;
+import com.cyksj.model.manage.views.CorpFollowMsgView;
+import com.ejlchina.searcher.SearchResult;
+
+/**
+ * @author chan
+ * @date 2024/1/22 15:07
+ */
+public interface CorpQyMsgContentService extends IService<QyMsgContent> {
+}

+ 11 - 0
netflix-service/src/main/java/com/cyksj/service/corp/WxCorpOps.java

@@ -39,6 +39,12 @@ public interface WxCorpOps {
 	 */
 	String getAccessToken(String corpId) throws Exception;
 
+	/**
+	 * 通过企业微信id 获取消息accessstoken
+	 * @param corpId
+	 */
+	String getMsgAccessToken(String corpId) throws Exception;
+
 	/**
 	 * 获取企业标签库
 	 *
@@ -177,4 +183,9 @@ public interface WxCorpOps {
 	 * 删除获客链接
 	 */
 	WxCpLinkAddResp customerAcquisitionDeleteLink(String corpId, WxCpLinkUpd wxCpLinkUpd) throws Exception;
+
+	/**
+	 * 获取会话内容存档开启成员列表
+	 */
+	List<String> getPermitUserList(String corpId, String type) throws Exception;
 }

+ 23 - 0
netflix-service/src/main/java/com/cyksj/service/corp/impl/CorpEventActionServiceImpl.java

@@ -544,6 +544,29 @@ public class CorpEventActionServiceImpl implements CorpEventActionService {
 		return null;
 	}
 
+	@Override
+	public CorpXmlOutMessage msgAuditApproved(CorpXmlMessage message, Map<String, Object> context) {
+		//修改 会话存档同意状态
+		String corpId = message.getToUserName();
+		String userId = message.getUserId();
+		String externalUserId = message.getExternalUserId();
+		//客服动态二维码配置state
+		WxCorpApp wxCorpApp = WeChatCorpFactory.getCorpConfigStrategy(corpId);
+		CorpUserFollowRelation relation = relationMapper
+				.selectOne(Wrappers.lambdaQuery(CorpUserFollowRelation.class)
+						.eq(CorpUserFollowRelation::getWxCorpId, wxCorpApp.getId())
+						.eq(CorpUserFollowRelation::getCorpUserId, externalUserId)
+						.eq(CorpUserFollowRelation::getFollowId, userId)
+						.last("limit 1"));
+		if(relation != null){
+			relation.setAgreeStatus("Agree");
+			relationMapper.updateById(relation);
+		}else {
+			throw  BusinessRuntimeException.getInstance("用户还未保存.");
+		}
+		return null;
+	}
+
 	/**
 	 * 修改客户成员关系,客户标签
 	 */

+ 15 - 0
netflix-service/src/main/java/com/cyksj/service/corp/impl/CorpMsgFollowAssociationServiceImpl.java

@@ -0,0 +1,15 @@
+package com.cyksj.service.corp.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.cyksj.mapper.corp.CorpMsgFollowAssociationMapper;
+import com.cyksj.model.entity.CorpMsgFollowAssociation;
+import com.cyksj.service.corp.CorpMsgFollowAssociationService;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author chan
+ * @date 2024/1/18 16:18
+ */
+@Service
+public class CorpMsgFollowAssociationServiceImpl extends ServiceImpl<CorpMsgFollowAssociationMapper, CorpMsgFollowAssociation> implements CorpMsgFollowAssociationService {
+}

+ 28 - 0
netflix-service/src/main/java/com/cyksj/service/corp/impl/CorpQyMsgContentServiceImpl.java

@@ -0,0 +1,28 @@
+package com.cyksj.service.corp.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.cyksj.mapper.corp.QyMsgContentMapper;
+import com.cyksj.model.entity.QyMsgContent;
+import com.cyksj.model.manage.views.CorpFollowMsgView;
+import com.cyksj.service.corp.CorpQyMsgContentService;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.util.MapBuilder;
+import com.ejlchina.searcher.util.MapUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author chan
+ * @date 2024/1/22 15:08
+ */
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class CorpQyMsgContentServiceImpl extends ServiceImpl<QyMsgContentMapper, QyMsgContent> implements CorpQyMsgContentService {
+
+    private final BeanSearcher beanSearcher;
+
+}

+ 16 - 2
netflix-service/src/main/java/com/cyksj/service/corp/impl/CorpServiceImpl.java

@@ -145,7 +145,17 @@ public class CorpServiceImpl implements CorpService {
 						return corpEventActionService.delCorpTag(message, context);
 					}
 				})
-				.end();
+				.end()
+				//用户同意会话存档
+				.rule().async(false).changeType("msg_audit_approved").event("change_external_contact")
+				.handler(new CorpMessageHandler() {
+					@Override
+					public CorpXmlOutMessage handle(CorpXmlMessage message, Map<String, Object> context, CorpService corpService) throws Exception {
+						return corpEventActionService.msgAuditApproved(message, context);
+					}
+				})
+				.end()
+		;
 
 	}
 
@@ -258,12 +268,16 @@ public class CorpServiceImpl implements CorpService {
 		WxCorpApp wxCorpApp = WeChatCorpFactory.getCorpConfigStrategy(corpId);
 		Long wxCorpId = wxCorpApp.getId();
 		CorpFollow corpFollow = corpFollowMapper.selectOne(Wrappers.lambdaQuery(CorpFollow.class).eq(CorpFollow::getWxCorpId, wxCorpId).eq(CorpFollow::getUserid, corpOauth2UserInfo.getUserId()));
+		CorpUserInfo userInfo = wxCorpOps.getFollowUser(corpId, corpOauth2UserInfo.getUserId());
 		if (corpFollow == null) {
-			CorpUserInfo userInfo = wxCorpOps.getFollowUser(corpId, corpOauth2UserInfo.getUserId());
 			corpFollow = new CorpFollow();
 			BeanUtil.copyProperties(userInfo, corpFollow);
 			corpFollow.setWxCorpId(wxCorpId);
 			corpFollowMapper.insert(corpFollow);
+		}else {
+			BeanUtil.copyProperties(userInfo, corpFollow);
+			corpFollow.setWxCorpId(wxCorpId);
+			corpFollowMapper.updateById(corpFollow);
 		}
 
 		return corpFollow;

+ 69 - 1
netflix-service/src/main/java/com/cyksj/service/corp/impl/WxCorpOpsImpl.java

@@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HttpUtil;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.cyksj.common.exception.BusinessRuntimeException;
 import com.cyksj.common.util.IoKit;
 import com.cyksj.common.util.J11HttpC;
@@ -84,6 +85,15 @@ public class WxCorpOpsImpl implements WxCorpOps {
 		}
 		return access_token(corpId);
 	}
+	@Override
+	public String getMsgAccessToken(String corpId) throws Exception {
+		corpId = Optional.ofNullable(corpId).orElse(yhlXWxCorpConfig.getCorpId());
+		String accessToken = redisService.getStr(RedisKey.CORP_MSG_ACCESS_TOKEN_KEY + corpId);
+		if (StringUtils.isNotBlank(accessToken)) {
+			return accessToken;
+		}
+		return getKfAccessToken(corpId);
+	}
 
 	@Override
 	public CorpUserExternalTagGroupList getCorpTagList(String corpId, List<String> tagIds, List<String> groupIds) throws Exception {
@@ -638,6 +648,38 @@ public class WxCorpOpsImpl implements WxCorpOps {
 		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 =
@@ -740,7 +782,7 @@ public class WxCorpOpsImpl implements WxCorpOps {
 
 	@Override
 	public WxCpLinkAddResp customerAcquisitionDeleteLink(String corpId, WxCpLinkUpd wxCpLinkUpd) throws Exception {
-		String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/delete_link?access_token=" + getAccessToken(corpId);
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/delete_link?access_token=";
 
 		HttpResponse<String> res =
 				J11HttpC.custom()
@@ -759,4 +801,30 @@ public class WxCorpOpsImpl implements WxCorpOps {
 		}
 		return resp;
 	}
+
+	@Override
+	public List<String> getPermitUserList(String corpId, String type) throws Exception {
+		String url = "https://qyapi.weixin.qq.com/cgi-bin/msgaudit/get_permit_user_list?access_token=" + getMsgAccessToken(corpId);
+
+		JSONObject params = new JSONObject();
+		if (StringUtils.isNotBlank(type)) {
+			params.putOpt("type",type);
+		}
+		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());
+		}
+
+		CorpBaseResult resp = Jsons.parseObject(res.body(), CorpBaseResult.class);
+		if (resp.getErrcode() != 0) {
+			throw BusinessRuntimeException.getInstance("获取会话内容存档开启成员列表: " + resp.getErrmsg());
+		}
+		return JSONUtil.parseObj(res.body()).getJSONArray("ids").toList(String.class);
+	}
 }

+ 2 - 0
netflix-service/src/main/java/com/cyksj/service/corp/msg/CorpEventActionService.java

@@ -29,4 +29,6 @@ public interface CorpEventActionService {
 	CorpXmlOutMessage updateCorpGroupChat(CorpXmlMessage message, Map<String, Object> context) throws Exception;
 
 	CorpXmlOutMessage delCorpGroupChat(CorpXmlMessage message);
+
+    CorpXmlOutMessage msgAuditApproved(CorpXmlMessage message, Map<String, Object> context);
 }

+ 99 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/CorpMsgAuditController.java

@@ -0,0 +1,99 @@
+package com.cyksj.web.controller.manage.corp;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.entity.QyMsgContent;
+import com.cyksj.model.manage.views.CorpFollowMsgView;
+import com.cyksj.model.manage.views.CorpMsgAuditUserView;
+import com.cyksj.service.corp.CorpQyMsgContentService;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.operator.InList;
+import com.ejlchina.searcher.param.Operator;
+import com.ejlchina.searcher.util.MapBuilder;
+import com.ejlchina.searcher.util.MapUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author chan
+ * @date 2024/1/22 14:25
+ */
+@RequiredArgsConstructor
+@Slf4j
+@RestController
+@RequestMapping("/manage/corp/msg/audit")
+public class CorpMsgAuditController {
+
+    private final BeanSearcher beanSearcher;
+
+    private final HttpServletRequest request;
+
+    private final CorpQyMsgContentService corpQyMsgContentService;
+
+
+
+    /**
+     * 会话存档用户列表
+     * @param textKey 关键词搜索
+     * @param tagId 标签筛选
+     * @return
+     */
+    @GetMapping
+    public Result<SearchResult<CorpMsgAuditUserView>> get(String textKey, String tagId,String followId) {
+        Set<String> userIds = new HashSet<>();
+        if(StringUtils.isNotBlank(textKey)){
+            List<QyMsgContent> text = corpQyMsgContentService.list(Wrappers.lambdaQuery(QyMsgContent.class).select(QyMsgContent::getFrom, QyMsgContent::getExternalUserid,QyMsgContent::getTolist).like(QyMsgContent::getText, textKey).eq(QyMsgContent::getRoomid,"").eq(QyMsgContent::getMsgtype, "text"));
+            userIds = text.stream().map(QyMsgContent::getExternalUserid).collect(Collectors.toSet());
+        }
+        MapBuilder builder = MapUtils.flatBuilder(request.getParameterMap());
+        if (userIds.size() > 0) {
+            builder.field(CorpMsgAuditUserView::getExternalUserid,userIds).op(InList.class);
+        }
+        if(StringUtils.isNotBlank(tagId)){
+            builder.put("tag", String.format( "and cr.id = t.relation_id and t.tag_id = %s", tagId));
+        }
+        if (StringUtils.isNotBlank(followId)) {
+            builder.field(CorpMsgAuditUserView::getFollowId, followId).op(Operator.Equal);
+        }
+        SearchResult<CorpMsgAuditUserView> search = beanSearcher.search(CorpMsgAuditUserView.class, builder.build());
+        return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+
+    }
+
+    /**
+     * 获取会话列表
+     */
+    @GetMapping("/msgList")
+    public Result<SearchResult<CorpFollowMsgView>> msgList(String keyword){
+        MapBuilder builder = MapUtils.flatBuilder(request.getParameterMap());
+        if(StringUtils.isNotBlank(keyword)){
+            builder.put("keyword", String.format("where q.text like '%s%%')",keyword));
+        }
+        SearchResult<CorpFollowMsgView> search = beanSearcher.search(CorpFollowMsgView.class, builder.build());
+        return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+    }
+
+
+    /**
+     * 获取聊天记录
+     */
+    @GetMapping("/getMsgByFollowIdAndUserId")
+    public Result<SearchResult<QyMsgContent>> getMsgByFollowIdAndUserId(){
+        SearchResult<QyMsgContent> search = beanSearcher.search(QyMsgContent.class, MapUtils.flatBuilder(request.getParameterMap()).build());
+        return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+    }
+
+
+}

+ 99 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/CorpMsgFollowController.java

@@ -0,0 +1,99 @@
+package com.cyksj.web.controller.manage.corp;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.config.corp.YHLXWxCorpConfig;
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.entity.CorpMsgFollowAssociation;
+import com.cyksj.model.manage.views.CorpMsgFollowAssociationView;
+import com.cyksj.service.corp.CorpMsgFollowAssociationService;
+import com.cyksj.service.corp.WxCorpOps;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.param.Operator;
+import com.ejlchina.searcher.util.MapUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author chan
+ * @date 2024/1/18 12:14
+ */
+@Slf4j
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/manage/msg/follow")
+public class CorpMsgFollowController {
+
+    private final CorpMsgFollowAssociationService corpMsgFollowAssociationService;
+
+    private final BeanSearcher beanSearcher;
+
+    private final HttpServletRequest request;
+
+    private final WxCorpOps wxCorpOps;
+
+    private final YHLXWxCorpConfig corpConfig;
+
+    /**
+     *  会话存档开启成员列表
+     * @author chan
+     * @date 2024-01-18 17:27
+     */
+    @GetMapping
+    public Result<SearchResult<CorpMsgFollowAssociationView>> getPermitUserList() throws Exception {
+        List<String> res = wxCorpOps.getPermitUserList(corpConfig.getCorpId(), null);
+        SearchResult<CorpMsgFollowAssociationView> search = beanSearcher.search(CorpMsgFollowAssociationView.class, MapUtils.flatBuilder(request.getParameterMap()).field(CorpMsgFollowAssociationView::getUserid, res).op(Operator.InList).build());
+        search.getDataList().forEach((data)->{
+            List<CorpMsgFollowAssociation> corpMsgFollowAssociations = beanSearcher.searchAll(CorpMsgFollowAssociation.class, MapUtils.builder().field(CorpMsgFollowAssociation::getFollowId, data.getUserid()).build());
+            List<String> followIds = corpMsgFollowAssociations.stream().map(CorpMsgFollowAssociation::getAssociationFollowId).collect(Collectors.toList());
+            if(followIds.size() > 0 ){
+                List<CorpMsgFollowAssociationView> corpMsgFollowAssociationViews = beanSearcher.searchAll(CorpMsgFollowAssociationView.class, MapUtils.builder().field(CorpMsgFollowAssociationView::getUserid, followIds).op(Operator.InList).build());
+                data.setAssociationList(corpMsgFollowAssociationViews);
+            }else {
+                data.setAssociationList(new ArrayList<>());
+            }
+        });
+        return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+    }
+
+    /**
+     *
+     * @return
+     */
+    @GetMapping("/all")
+    public Result<List<CorpMsgFollowAssociationView>> getAll() throws Exception {
+        List<String> res = wxCorpOps.getPermitUserList(corpConfig.getCorpId(), null);
+        List<CorpMsgFollowAssociationView> search = beanSearcher.searchAll(CorpMsgFollowAssociationView.class, MapUtils.flatBuilder(request.getParameterMap()).field(CorpMsgFollowAssociationView::getUserid, res).op(Operator.InList).build());
+
+        return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+    }
+
+    /**
+     * 新增会话存档关系
+     * @author chan
+     * @date 2024-01-18 17:26
+     */
+    @PostMapping
+    public Result<String> addAssociation(@RequestBody CorpMsgFollowAssociation corpMsgFollowAssociation){
+        corpMsgFollowAssociationService.save(corpMsgFollowAssociation);
+        return GatewayResponse.SUCCESS.newBuilder().toResult();
+    }
+
+    /**
+     * 删除会话存档关系
+     * @author chan
+     * @date 2024-01-18 17:26
+     */
+    @DeleteMapping
+    public Result<String> delAssociation(@RequestBody CorpMsgFollowAssociation corpMsgFollowAssociation){
+        corpMsgFollowAssociationService.remove(Wrappers.lambdaQuery(CorpMsgFollowAssociation.class).eq(CorpMsgFollowAssociation::getAssociationFollowId,corpMsgFollowAssociation.getAssociationFollowId()).eq(CorpMsgFollowAssociation::getFollowId,corpMsgFollowAssociation.getFollowId()));
+        return GatewayResponse.SUCCESS.newBuilder().toResult();
+    }
+}

+ 41 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/CorpUserController.java

@@ -0,0 +1,41 @@
+package com.cyksj.web.controller.manage.corp;
+
+import com.cyksj.dto.Result;
+import com.cyksj.enums.GatewayResponse;
+import com.cyksj.model.manage.views.CorpUserView;
+import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.util.MapUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 企业微信/用户列表
+ * @author chan
+ * @date 2024/1/19 10:48
+ */
+@Slf4j
+@RestController
+@RequestMapping("/manage/corp/user")
+@RequiredArgsConstructor
+public class CorpUserController {
+
+    private final BeanSearcher beanSearcher;
+
+    private final HttpServletRequest request;
+
+    /**
+     * 企业微信用户列表
+     */
+    @GetMapping
+    public Result<SearchResult<CorpUserView>> get(){
+        SearchResult<CorpUserView> search = beanSearcher.search(CorpUserView.class, MapUtils.flatBuilder(request.getParameterMap()).build());
+        return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+    }
+
+}

+ 41 - 0
netflix-web/src/main/java/com/cyksj/web/controller/manage/corp/WxCorpController.java

@@ -4,6 +4,7 @@ import cn.hutool.core.date.DateTime;
 import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.HttpUtil;
+import com.cyksj.common.exception.BusinessRuntimeException;
 import com.cyksj.common.snowflake.Sequence;
 import com.cyksj.common.util.Codec;
 import com.cyksj.common.util.IoKit;
@@ -18,12 +19,18 @@ import com.cyksj.mapper.GroupsRelationCustomerServiceFollowRecordMapper;
 import com.cyksj.mapper.GroupsRelationExpiryRecordMapper;
 import com.cyksj.model.dto.AuthRedirect;
 import com.cyksj.model.entity.CorpFollow;
+import com.cyksj.model.entity.CorpMsgFollowAssociation;
+import com.cyksj.model.entity.QyMsgContent;
+import com.cyksj.model.manage.views.CorpQyMsgAuditChatView;
 import com.cyksj.model.request.corp.CorpCommonReq;
 import com.cyksj.model.views.YHLXUserDetailView;
 import com.cyksj.redis.RedisService;
 import com.cyksj.service.corp.CorpService;
 import com.cyksj.service.corp.CorpUserService;
 import com.ejlchina.searcher.BeanSearcher;
+import com.ejlchina.searcher.SearchResult;
+import com.ejlchina.searcher.param.Operator;
+import com.ejlchina.searcher.util.MapUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -33,8 +40,10 @@ import org.springframework.web.bind.annotation.*;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.stream.Collectors;
 
 /*
  *项目名: netflix
@@ -260,4 +269,36 @@ public class WxCorpController {
 		corpUserService.markCorpUser(corpCommonReq);
 		return GatewayResponse.SUCCESS.newBuilder().toResult();
 	}
+
+
+	/**
+	 * 企微会话列表
+	 * @param externalUserid 用户 externalUserid
+	 */
+	@GetMapping("/getMsgAuditChat")
+	public Result<List<CorpQyMsgAuditChatView>> getMsgAuditChat(String externalUserid) {
+		String followId = getCorpUuid();
+		List<CorpMsgFollowAssociation> corpMsgFollowAssociations = beanSearcher.searchAll(CorpMsgFollowAssociation.class, MapUtils.builder().field(CorpMsgFollowAssociation::getFollowId, followId).build());
+		List<String> followIds = corpMsgFollowAssociations.stream().map(CorpMsgFollowAssociation::getAssociationFollowId).collect(Collectors.toList());
+		followIds.add(followId);
+		if (StringUtils.isBlank(externalUserid)) {
+			throw BusinessRuntimeException.getInstance("缺少必填参数.");
+		}
+		List<CorpQyMsgAuditChatView> search = beanSearcher.searchAll(CorpQyMsgAuditChatView.class, MapUtils.flatBuilder(request.getParameterMap())
+				.field("followId", followIds.stream()
+						.map(String::valueOf)
+						.collect(Collectors.toList())).op(Operator.InList)
+				.build());
+		return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+	}
+
+
+	/**
+	 * 获取聊天记录
+	 */
+	@GetMapping("/getMsgByFollowIdAndUserId")
+	public Result<SearchResult<QyMsgContent>> getMsgByFollowIdAndUserId(){
+		SearchResult<QyMsgContent> search = beanSearcher.search(QyMsgContent.class, MapUtils.flatBuilder(request.getParameterMap()).build());
+		return GatewayResponse.SUCCESS.newBuilder().toResult(search);
+	}
 }