Pārlūkot izejas kodu

feat:企微会话存档(存储四种类型的消息、语音转格式后上传到oss、语音转文字存储)

caoliqin 2 nedēļas atpakaļ
vecāks
revīzija
ceb8fd4194
21 mainītis faili ar 1393 papildinājumiem un 4 dzēšanām
  1. 144 0
      fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/QwMsgAuditIngestService.java
  2. 66 0
      fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/QwMsgAuditPullService.java
  3. 56 0
      fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/QwMsgAuditWxCpFactory.java
  4. 112 0
      fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/QwMsgMediaFileService.java
  5. 32 0
      fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/handler/ImageMsgHandler.java
  6. 44 0
      fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/handler/QwMsgAuditMsgHandler.java
  7. 32 0
      fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/handler/TextMsgHandler.java
  8. 33 0
      fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/handler/VideoMsgHandler.java
  9. 33 0
      fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/handler/VoiceMsgHandler.java
  10. 118 0
      fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/job/QwMsgAuditScheduleJob.java
  11. 31 4
      fs-qw-api-msg/src/main/java/com/tencent/wework/Finance.java
  12. 34 0
      fs-service/src/main/java/com/fs/enums/MediaMsgTypeEnum.java
  13. 48 0
      fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditMessage.java
  14. 31 0
      fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditRaw.java
  15. 21 0
      fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditSeq.java
  16. 75 0
      fs-service/src/main/java/com/fs/qw/mapper/QwMsgAuditMessageMapper.java
  17. 69 0
      fs-service/src/main/java/com/fs/qw/mapper/QwMsgAuditRawMapper.java
  18. 75 0
      fs-service/src/main/java/com/fs/qw/mapper/QwMsgAuditSeqMapper.java
  19. 148 0
      fs-service/src/main/resources/mapper/qw/QwMsgAuditMessageMapper.xml
  20. 113 0
      fs-service/src/main/resources/mapper/qw/QwMsgAuditRawMapper.xml
  21. 78 0
      fs-service/src/main/resources/mapper/qw/QwMsgAuditSeqMapper.xml

+ 144 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/QwMsgAuditIngestService.java

@@ -0,0 +1,144 @@
+package com.fs.app.msgarchives;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.app.msgarchives.handler.QwMsgAuditMsgHandler;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import com.fs.qw.domain.audit.QwMsgAuditRaw;
+import com.fs.qw.domain.audit.QwMsgAuditSeq;
+import com.fs.qw.mapper.QwCompanyMapper;
+import com.fs.qw.mapper.QwMsgAuditMessageMapper;
+import com.fs.qw.mapper.QwMsgAuditRawMapper;
+import com.fs.qw.mapper.QwMsgAuditSeqMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+
+/**
+ * 会话存档,步骤:拉取 -> 解密 -> 存入库表
+ *
+ * @author caoliqin
+ */
+@Service
+@Slf4j
+public class QwMsgAuditIngestService {
+
+    private final QwCompanyMapper qwCompanyMapper;
+    private final QwMsgAuditSeqMapper seqMapper;
+    private final QwMsgAuditRawMapper rawMapper;
+    private final QwMsgAuditMessageMapper messageMapper;
+    private final QwMsgAuditPullService pullService;
+    private final List<QwMsgAuditMsgHandler> handlers;
+
+    public QwMsgAuditIngestService(QwCompanyMapper qwCompanyMapper,
+                                   QwMsgAuditSeqMapper seqMapper,
+                                   QwMsgAuditRawMapper rawMapper,
+                                   QwMsgAuditMessageMapper messageMapper,
+                                   QwMsgAuditPullService pullService,
+                                   List<QwMsgAuditMsgHandler> handlers) {
+        this.qwCompanyMapper = qwCompanyMapper;
+        this.seqMapper = seqMapper;
+        this.rawMapper = rawMapper;
+        this.messageMapper = messageMapper;
+        this.pullService = pullService;
+        this.handlers = handlers;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void ingestOnce(String corpId, long limit) throws Exception {
+        // 1、判断相关参数
+        QwCompany company = qwCompanyMapper.selectQwCompanyByCorpId(corpId);
+        if (company == null) {
+            log.warn("会话存档 ingest 跳过,原因:corpId不存在,corpId={}", corpId);
+            return;
+        }
+        if (company.getMsgSecret() == null || company.getMsgSecret().isEmpty()) {
+            log.warn("会话存档 ingest 跳过,原因:msgSecret为空,corpId={}", corpId);
+            return;
+        }
+        if (company.getMsgPrivateKey() == null || company.getMsgPrivateKey().isEmpty()) {
+            log.warn("会话存档 ingest 跳过,原因:msgPrivateKey为空,corpId={}", corpId);
+            return;
+        }
+
+        // 2、拉取数据
+        // 获取已成功抓取到的最大seq
+        QwMsgAuditSeq qwMsgAuditSeq = seqMapper.selectByCorpId(corpId);
+        if (qwMsgAuditSeq == null) {
+            qwMsgAuditSeq = new QwMsgAuditSeq();
+            qwMsgAuditSeq.setCorpId(corpId);
+            qwMsgAuditSeq.setSeq(0L);
+            qwMsgAuditSeq.setCreateTime(new Date());
+            qwMsgAuditSeq.setUpdateTime(new Date());
+            seqMapper.insertQwMsgAuditSeq(qwMsgAuditSeq);
+        }
+
+        long lastSeq = qwMsgAuditSeq.getSeq() == null ? 0L : qwMsgAuditSeq.getSeq();
+        List<QwMsgAuditPullService.PlainAuditMsg> batch = pullService.pullOnce(company, lastSeq, limit);
+        if (batch.isEmpty()) {
+            return;
+        }
+
+        // 3、数据解密,并存入数据库
+        for (QwMsgAuditPullService.PlainAuditMsg item : batch) {
+            Long seq = item.getSeq();
+            String plainJson = item.getPlainJson();
+
+            if (seq == null || plainJson == null || plainJson.isEmpty()) {
+                continue;
+            }
+
+            // 通过 corp_id + seq 查询唯一
+            QwMsgAuditRaw existsRaw = rawMapper.selectByCorpIdAndSeq(corpId, seq);
+            Long rawId;
+            if (existsRaw != null) {
+                rawId = existsRaw.getId();
+            } else {
+                JSONObject plain = JSON.parseObject(plainJson);
+                QwMsgAuditRaw raw = new QwMsgAuditRaw();
+                raw.setCorpId(corpId);
+                raw.setSeq(seq);
+                raw.setMsgId(plain.getString("msgid"));
+                raw.setAction(plain.getString("action"));
+                raw.setFromUser(plain.getString("from"));
+                raw.setToList(plain.getJSONArray("tolist") == null ? null : plain.getJSONArray("tolist").toJSONString());
+                raw.setRoomId(plain.getString("roomid"));
+                raw.setMsgTime(plain.getLong("msgtime"));
+                raw.setMsgType(plain.getString("msgtype"));
+                raw.setRawJson(plainJson);
+                raw.setCreateTime(new Date());
+                rawMapper.insertQwMsgAuditRaw(raw);
+                rawId = raw.getId();
+            }
+
+            // 结构化消息:暂时只处理 text/voice
+            JSONObject plain = JSON.parseObject(plainJson);
+            String msgType = plain.getString("msgtype");
+            Optional<QwMsgAuditMsgHandler> handlerOpt = handlers.stream().filter(h -> h.supports(msgType)).findFirst();
+            // 如果找不到,则跳出
+            if (!handlerOpt.isPresent()) {
+                continue;
+            }
+
+            QwMsgAuditMessage existsMsg = messageMapper.selectByCorpIdAndSeq(corpId, seq);
+            if (existsMsg != null) {
+                continue;
+            }
+            QwMsgAuditMessage msg = handlerOpt.get().buildMessage(corpId, seq, rawId, plain);
+            messageMapper.insertQwMsgAuditMessage(msg);
+        }
+
+        // 4、最后,更新当前批次最大的seq
+        Long maxSeq = batch.stream()
+                .map(QwMsgAuditPullService.PlainAuditMsg::getSeq)
+                .filter(Objects::nonNull)
+                .max(Comparator.naturalOrder())
+                .orElse(lastSeq);
+
+        seqMapper.advanceSeq(corpId, maxSeq);
+    }
+}
+

+ 66 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/QwMsgAuditPullService.java

@@ -0,0 +1,66 @@
+package com.fs.app.msgarchives;
+
+import com.fs.qw.domain.QwCompany;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.msgaudit.WxCpChatDatas;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 拉取并解密会话存档内容
+ * @author caoliqin
+ */
+@Service
+@Slf4j
+public class QwMsgAuditPullService {
+
+    private final QwMsgAuditWxCpFactory factory;
+
+    public QwMsgAuditPullService(QwMsgAuditWxCpFactory factory) {
+        this.factory = factory;
+    }
+
+    @Data
+    @AllArgsConstructor
+    public static class PlainAuditMsg {
+        private Long seq;
+        private String plainJson;
+    }
+
+    /**
+     * 从 lastSeq 之后拉取一批,最后返回明文 JSON列表
+     * 注意:下次拉取需要从 seq+1 开始返回
+     */
+    public List<PlainAuditMsg> pullOnce(QwCompany company, long lastSeq, long limit) throws Exception {
+        // 根据lastSeq 获取 WxCpChatDatas
+        WxCpService cpService = factory.create(company);
+        WxCpChatDatas chatDatas = cpService.getMsgAuditService().getChatDatas(lastSeq, limit, null, null, 15_000L);
+        List<PlainAuditMsg> result = new ArrayList<>();
+        if (chatDatas == null || chatDatas.getChatData() == null || chatDatas.getChatData().isEmpty()) {
+            return result;
+        }
+
+        long sdk = chatDatas.getSdk() == null ? 0L : chatDatas.getSdk();
+
+        // 遍历数据,使用PKCS8 格式的私钥解密获取明文
+        int pkcsType = 2;
+        for (Object item : chatDatas.getChatData()) {
+            WxCpChatDatas.WxCpChatData chatData = (WxCpChatDatas.WxCpChatData) item;
+            Long seq = chatData.getSeq();
+            // 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ...
+            String plain = cpService.getMsgAuditService().getChatPlainText(sdk, chatData, pkcsType);
+            if (plain != null && !plain.isEmpty()) {
+                result.add(new PlainAuditMsg(seq, plain));
+            } else {
+                log.warn("会话存档解密结果为空 corpId={}, seq={}", company.getCorpId(), seq);
+            }
+        }
+        return result;
+    }
+}
+

+ 56 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/QwMsgAuditWxCpFactory.java

@@ -0,0 +1,56 @@
+package com.fs.app.msgarchives;
+
+import com.fs.qw.domain.QwCompany;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl;
+import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+/**
+ * 构建会话存档客户端
+ * 按corpId+msgSecret+私钥,动态构建会话存档客户端
+ * @author caoliqin
+ */
+@Component
+@Slf4j
+public class QwMsgAuditWxCpFactory {
+
+    /**
+     * 会话存档 Finance SDK 路径(WxJava 要求)
+     */
+    @Value("${qw.msg-audit.sdk-lib-path:}")
+    private String msgAuditSdkLibPath;
+
+    public WxCpService create(QwCompany company) {
+        // 创建企微配置对象,常用
+        WxCpDefaultConfigImpl config = new WxCpDefaultConfigImpl();
+        config.setCorpId(company.getCorpId());
+
+        // 会话存档的secret用于获取access_token 以及调用 msgaudit 接口
+        // 设置企业微信的secret,确保取到正确配置
+        config.setCorpSecret(company.getMsgSecret());
+        config.setMsgAuditSecret(company.getMsgSecret());
+
+        // 解密会话存档用的私钥(PEM)
+        config.setMsgAuditPriKey(company.getMsgPrivateKey());
+
+        // agentId对会话存档拉取不是必须,但 WxCpConfigStorage 接口要求 不为 null
+        if (company.getAgentId() != null) {
+            config.setAgentId(company.getAgentId());
+        } else {
+            config.setAgentId(0);
+        }
+
+        if (!StringUtils.hasText(msgAuditSdkLibPath)) {
+            throw new IllegalStateException("未配置会话存档 Finance SDK 路径");
+        }
+        config.setMsgAuditLibPath(msgAuditSdkLibPath.trim());
+
+        WxCpService service = new WxCpServiceImpl();
+        service.setWxCpConfigStorage(config);
+        return service;
+    }
+}

+ 112 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/QwMsgMediaFileService.java

@@ -0,0 +1,112 @@
+package com.fs.app.msgarchives;
+
+import com.fs.qw.domain.QwCompany;
+import com.fs.utils.AmrToMp3Util;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import com.tencent.wework.Finance;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Locale;
+
+/**
+ * 企业微信会话存档:Finance 下载媒体并上传 OSS。
+ * 语音为 amr 时可先转 mp3 再上传,供豆包 ASR 使用。
+ *
+ * @author caoliqin
+ */
+@Service
+@Slf4j
+public class QwMsgMediaFileService {
+
+    @Value("${qw.msg-audit.ffmpeg-path}")
+    private String ffmpegPath;
+
+    @Value("${qw.msg-audit.voice-amr-to-mp3:true}")
+    private boolean voiceAmrToMp3Enabled;
+
+    /**
+     * 下载后上传 OSS。后缀为 .amr 且开启转换时,会先转为 mp3 再上传,返回的 URL 指向 mp3。
+     *
+     * @param company    企业信息(需要 corpId 和 msgSecret)
+     * @param sdkfileid  会话存档消息中的媒体文件id
+     * @param fileSuffix 文件后缀,由企微提供,如 .amr / .mp4 / .jpg
+     * @return OSS 存储URL,失败返回 null
+     */
+    public String downloadAndUpload(QwCompany company, String sdkfileid, String fileSuffix) {
+        if (sdkfileid == null || sdkfileid.isEmpty()) {
+            return null;
+        }
+        long sdk = Finance.NewSdk();
+        long ret = Finance.Init(sdk, company.getCorpId(), company.getMsgSecret());
+        if (ret != 0) {
+            log.error("会话存档媒体下载Init失败, ret={}, corpId={}", ret, company.getCorpId());
+            Finance.DestroySdk(sdk);
+            return null;
+        }
+
+        try {
+            byte[] fileBytes = downloadMediaBytes(sdk, sdkfileid);
+            if (fileBytes == null || fileBytes.length == 0) {
+                log.error("会话存档媒体下载数据为空, sdkfileid={}", sdkfileid);
+                return null;
+            }
+            String uploadSuffix = fileSuffix;
+
+            // 转语音格式
+            if (voiceAmrToMp3Enabled && fileSuffix != null && fileSuffix.toLowerCase(Locale.ROOT).endsWith(".amr")) {
+                try {
+                    fileBytes = AmrToMp3Util.amrBytesToMp3Bytes(fileBytes, ffmpegPath);
+                    uploadSuffix = ".mp3";
+                } catch (Exception ex) {
+                    log.error("企微语音 amr 转 mp3 失败,改为上传原始 amr, sdkfileid={}", sdkfileid, ex);
+                }
+            }
+            CloudStorageService storage = OSSFactory.build();
+            String url = storage.uploadSuffix(fileBytes, uploadSuffix);
+            log.info("会话存档媒体上传OSS成功, sdkfileid={}, suffix={}, url={}", sdkfileid, uploadSuffix, url);
+            return url;
+        } catch (Exception e) {
+            log.error("会话存档媒体上传OSS失败, sdkfileid={}", sdkfileid, e);
+            return null;
+        } finally {
+            Finance.DestroySdk(sdk);
+        }
+    }
+
+    /**
+     * 循环分片拉取媒体二进制数据,直到 isFinish
+     */
+    private byte[] downloadMediaBytes(long sdk, String sdkfileid) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        String indexbuf = "";
+        while (true) {
+            long mediaData = Finance.NewMediaData();
+            try {
+                long ret = Finance.GetMediaData(sdk, indexbuf, sdkfileid, "", "", 15L, mediaData);
+                if (ret != 0) {
+                    log.error("Finance.GetMediaData调用失败, ret={}, sdkfileid={}", ret, sdkfileid);
+                    return null;
+                }
+                byte[] chunk = Finance.GetData(mediaData);
+                if (chunk != null && chunk.length > 0) {
+                    out.write(chunk, 0, chunk.length);
+                }
+                boolean isFinish = Finance.IsMediaDataFinish(mediaData) == 1;
+                if (isFinish) {
+                    break;
+                }
+                indexbuf = Finance.GetOutIndexBuf(mediaData);
+            } catch (Exception e) {
+                log.error("Finance.GetMediaData分片处理异常, sdkfileid={}", sdkfileid, e);
+                return null;
+            } finally {
+                Finance.FreeMediaData(mediaData);
+            }
+        }
+        return out.toByteArray();
+    }
+}

+ 32 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/handler/ImageMsgHandler.java

@@ -0,0 +1,32 @@
+package com.fs.app.msgarchives.handler;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import org.springframework.stereotype.Component;
+
+/**
+ * 语音消息处理
+ * @author caoliqin
+ */
+@Component
+public class ImageMsgHandler implements QwMsgAuditMsgHandler {
+    @Override
+    public boolean supports(String msgType) {
+        return "image".equals(msgType);
+    }
+
+    @Override
+    public QwMsgAuditMessage buildMessage(String corpId, Long seq, Long rawId, JSONObject plain) {
+        QwMsgAuditMessage msg = new QwMsgAuditMessage();
+        fillCommonFields(msg, corpId, seq, rawId, plain);
+
+        JSONObject image = plain.getJSONObject("image");
+        if (image != null) {
+            msg.setMediaSdkfileid(image.getString("sdkfileid"));
+            msg.setMediaMd5sum(image.getString("md5sum"));
+            msg.setMediaSize(image.getInteger("filesize"));
+        }
+        return msg;
+    }
+}
+

+ 44 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/handler/QwMsgAuditMsgHandler.java

@@ -0,0 +1,44 @@
+package com.fs.app.msgarchives.handler;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+
+import java.util.Date;
+
+/**
+ * 会话存档-消息处理策略(按 msgtype 扩展)
+ * @author caoliqin
+ */
+public interface QwMsgAuditMsgHandler {
+
+    /**
+     * msgtype 处理类型
+     */
+    boolean supports(String msgType);
+
+    /**
+     * 各个msgtype的公共字段,放在这里统一处理,避免每个 Handler重复set
+     * @param msg 消息对象
+     * @param corpId 企微id
+     * @param seq seq
+     * @param rawId 关联raw表主键
+     * @param plain
+     */
+    default void fillCommonFields(QwMsgAuditMessage msg, String corpId, Long seq, Long rawId, JSONObject plain) {
+        msg.setCorpId(corpId);
+        msg.setSeq(seq);
+        msg.setRawId(rawId);
+        msg.setCreateTime(new Date());
+        if (plain != null) {
+            msg.setMsgId(plain.getString("msgid"));
+            msg.setMsgTime(plain.getLong("msgtime"));
+            msg.setMsgType(plain.getString("msgtype"));
+        }
+    }
+
+    /**
+     * 从明文 JSON 解析出结构化字段
+     */
+    QwMsgAuditMessage buildMessage(String corpId, Long seq, Long rawId, JSONObject plain);
+}
+

+ 32 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/handler/TextMsgHandler.java

@@ -0,0 +1,32 @@
+package com.fs.app.msgarchives.handler;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import org.springframework.stereotype.Component;
+
+/**
+ * 文本消息处理
+ * @author caoliqin
+ */
+@Component
+public class TextMsgHandler implements QwMsgAuditMsgHandler {
+    @Override
+    public boolean supports(String msgType) {
+        return "text".equals(msgType);
+    }
+
+    @Override
+    public QwMsgAuditMessage buildMessage(String corpId, Long seq, Long rawId, JSONObject plain) {
+        QwMsgAuditMessage msg = new QwMsgAuditMessage();
+        // 设置公共值
+        fillCommonFields(msg, corpId, seq, rawId, plain);
+
+        JSONObject text = plain.getJSONObject("text");
+        if (text != null) {
+            msg.setTextContent(text.getString("content"));
+        }
+
+        return msg;
+    }
+}
+

+ 33 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/handler/VideoMsgHandler.java

@@ -0,0 +1,33 @@
+package com.fs.app.msgarchives.handler;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import org.springframework.stereotype.Component;
+
+/**
+ * 视频消息处理
+ * @author caoliqin
+ */
+@Component
+public class VideoMsgHandler implements QwMsgAuditMsgHandler {
+    @Override
+    public boolean supports(String msgType) {
+        return "video".equals(msgType);
+    }
+
+    @Override
+    public QwMsgAuditMessage buildMessage(String corpId, Long seq, Long rawId, JSONObject plain) {
+        QwMsgAuditMessage msg = new QwMsgAuditMessage();
+        fillCommonFields(msg, corpId, seq, rawId, plain);
+
+        JSONObject video = plain.getJSONObject("video");
+        if (video != null) {
+            msg.setMediaSdkfileid(video.getString("sdkfileid"));
+            msg.setMediaMd5sum(video.getString("md5sum"));
+            msg.setMediaSize(video.getInteger("filesize"));
+            msg.setMediaPlayLength(video.getInteger("play_length"));
+        }
+        return msg;
+    }
+}
+

+ 33 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/handler/VoiceMsgHandler.java

@@ -0,0 +1,33 @@
+package com.fs.app.msgarchives.handler;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import org.springframework.stereotype.Component;
+
+/**
+ * 语音消息处理
+ * @author caoliqin
+ */
+@Component
+public class VoiceMsgHandler implements QwMsgAuditMsgHandler {
+    @Override
+    public boolean supports(String msgType) {
+        return "voice".equals(msgType);
+    }
+
+    @Override
+    public QwMsgAuditMessage buildMessage(String corpId, Long seq, Long rawId, JSONObject plain) {
+        QwMsgAuditMessage msg = new QwMsgAuditMessage();
+        fillCommonFields(msg, corpId, seq, rawId, plain);
+
+        JSONObject voice = plain.getJSONObject("voice");
+        if (voice != null) {
+            msg.setMediaSdkfileid(voice.getString("sdkfileid"));
+            msg.setMediaMd5sum(voice.getString("md5sum"));
+            msg.setMediaSize(voice.getInteger("voice_size"));
+            msg.setMediaPlayLength(voice.getInteger("play_length"));
+        }
+        return msg;
+    }
+}
+

+ 118 - 0
fs-qw-api-msg/src/main/java/com/fs/app/msgarchives/job/QwMsgAuditScheduleJob.java

@@ -0,0 +1,118 @@
+package com.fs.app.msgarchives.job;
+
+import com.fs.aiSoundReplication.service.AsrService;
+import com.fs.app.msgarchives.QwMsgAuditIngestService;
+import com.fs.app.msgarchives.QwMsgMediaFileService;
+import com.fs.enums.MediaMsgTypeEnum;
+import com.fs.qw.domain.QwCompany;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import com.fs.qw.mapper.QwCompanyMapper;
+import com.fs.qw.mapper.QwMsgAuditMessageMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+/**
+ * 定时拉取会话存档消息并存库;
+ * 定时补充媒体文件(语音/视频/图片)OSS 上传;
+ */
+@Component
+@Slf4j
+@RequiredArgsConstructor
+public class QwMsgAuditScheduleJob {
+
+    /** 每次每种类型最多处理的条数,避免单次任务耗时过长 */
+    private static final int MEDIA_BATCH_LIMIT = 50;
+
+    private final QwCompanyMapper qwCompanyMapper;
+    private final QwMsgAuditIngestService ingestService;
+    private final QwMsgAuditMessageMapper messageMapper;
+    private final QwMsgMediaFileService mediaFileService;
+    private final AsrService asrService;
+
+    /**
+     * 每1分钟执行一次,拉取会话存档消息并存库
+     */
+    @Scheduled(cron = "0 */1 * * * ?")
+    public void pullQwMsgAndStore() {
+        List<String> corpIds = qwCompanyMapper.selectQwCompanyCorpIdListByAll();
+        if (corpIds == null || corpIds.isEmpty()) {
+            return;
+        }
+        for (String corpId : corpIds) {
+            try {
+                ingestService.ingestOnce(corpId, 1000);
+            } catch (Exception e) {
+                log.error("会话存档 ingest失败,corpId={}", corpId, e);
+            }
+        }
+    }
+
+    /**
+     * 每2分钟执行一次,扫描 media_oss_url为空的媒体消息(语音/视频/图片),下载并上传至 OSS
+     */
+    @Scheduled(cron = "0 */2 * * * ?")
+    public void uploadMediaToOss() {
+        List<String> corpIds = qwCompanyMapper.selectQwCompanyCorpIdListByAll();
+        if (corpIds == null || corpIds.isEmpty()) {
+            return;
+        }
+        for (String corpId : corpIds) {
+            QwCompany company = qwCompanyMapper.selectQwCompanyByCorpId(corpId);
+            if (company == null || company.getMsgSecret() == null || company.getMsgSecret().isEmpty()) {
+                log.warn("媒体OSS上传跳过,corpId={} 企业信息不完整", corpId);
+                continue;
+            }
+            for (MediaMsgTypeEnum type : MediaMsgTypeEnum.values()) {
+                try {
+                    processCorpMedia(company, type);
+                } catch (Exception e) {
+                    log.error("媒体OSS上传任务异常, corpId={}, type={}", corpId, type.getMsgType(), e);
+                }
+            }
+        }
+    }
+
+    private void processCorpMedia(QwCompany company, MediaMsgTypeEnum type) {
+        List<QwMsgAuditMessage> pendingList = messageMapper.selectMediaPendingOssUpload(
+                company.getCorpId(), type.getMsgType(), MEDIA_BATCH_LIMIT);
+        if (pendingList == null || pendingList.isEmpty()) {
+            return;
+        }
+        log.info("媒体OSS上传开始, corpId={}, type={}, 待处理条数={}", company.getCorpId(), type.getMsgType(), pendingList.size());
+        int successCount = 0;
+        for (QwMsgAuditMessage msg : pendingList) {
+            String ossUrl = mediaFileService.downloadAndUpload(company, msg.getMediaSdkfileid(), type.getFileSuffix());
+            if (ossUrl != null) {
+                msg.setMediaOssUrl(ossUrl);
+                messageMapper.updateQwMsgAuditMessage(msg);
+                successCount++;
+
+                //将语音转文字的内容存入数据库
+                if (MediaMsgTypeEnum.VOICE.equals(type)) {
+                    transcribeVoiceToDb(msg);
+                }
+            }
+        }
+        log.info("媒体OSS上传完成, corpId={}, type={}, 成功/总条数={}/{}", company.getCorpId(), type.getMsgType(), successCount, pendingList.size());
+    }
+
+    /**
+     * 语音上传 OSS 后调用豆包 ASR,将识别结果更新到表
+     */
+    private void transcribeVoiceToDb(QwMsgAuditMessage msg) {
+        try {
+            String text = asrService.recognizeFromUrl(msg.getMediaOssUrl());
+            if (StringUtils.hasText(text)) {
+                msg.setTextContent(text);
+                messageMapper.updateQwMsgAuditMessage(msg);
+            }
+        } catch (Exception e) {
+            log.warn("语音转文字失败, msgId={}, id={}", msg.getMsgId(), msg.getId(), e);
+        }
+    }
+}

+ 31 - 4
fs-qw-api-msg/src/main/java/com/tencent/wework/Finance.java

@@ -1,5 +1,8 @@
 package com.tencent.wework;
 
+import java.io.File;
+import java.util.List;
+
 /* sdk
 typedef struct Slice_t {
     char* buf;
@@ -16,6 +19,34 @@ typedef struct MediaData {
 */
 
 public class Finance {
+
+    /**
+     * WxJava 4.x 会话存档拉取前会调用;按 msgAuditLibPath 显式加载 dll。
+     */
+    public static Finance loadingLibraries(List<String> libraries, String libDirOrFirstLibPath) {
+        if (libraries == null || libraries.isEmpty()) {
+            return new Finance();
+        }
+        final File base;
+        if (libDirOrFirstLibPath == null || libDirOrFirstLibPath.trim().isEmpty()) {
+            base = null;
+        } else {
+            File f = new File(libDirOrFirstLibPath);
+            base = f.isFile() ? f.getParentFile() : f;
+        }
+        for (String lib : libraries) {
+            if (lib == null || lib.trim().isEmpty()) {
+                continue;
+            }
+            File libFile = new File(lib);
+            if (!libFile.isAbsolute() && base != null) {
+                libFile = new File(base, lib);
+            }
+            System.load(libFile.getAbsolutePath());
+        }
+        return new Finance();
+    }
+
     public native static long NewSdk();
 
     /**
@@ -121,8 +152,4 @@ public class Finance {
      * @brief �ж�mediadata�Ƿ����
      */
     public native static int IsMediaDataFinish(long mediaData);
-
-    static {
-        System.loadLibrary("WeWorkFinanceSdk");
-    }
 }

+ 34 - 0
fs-service/src/main/java/com/fs/enums/MediaMsgTypeEnum.java

@@ -0,0 +1,34 @@
+package com.fs.enums;
+
+import lombok.Getter;
+
+/**
+ * 企业微信会话存档 - 需要下载并上传至 OSS 的媒体消息类型
+ */
+@Getter
+public enum MediaMsgTypeEnum {
+
+    /**
+     * 语音
+     */
+    VOICE("voice", ".amr"),
+    /**
+     * 视频
+     */
+    VIDEO("video", ".mp4"),
+    /**
+     * 图片
+     */
+    IMAGE("image", ".jpg");
+
+    /** 企微消息中的 msgtype 字段值 */
+    private final String msgType;
+
+    /** 上传 OSS 时使用的文件后缀 */
+    private final String fileSuffix;
+
+    MediaMsgTypeEnum(String msgType, String fileSuffix) {
+        this.msgType = msgType;
+        this.fileSuffix = fileSuffix;
+    }
+}

+ 48 - 0
fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditMessage.java

@@ -0,0 +1,48 @@
+package com.fs.qw.domain.audit;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 企业微信会话存档结构化消息 qw_msg_audit_message
+ */
+@Data
+public class QwMsgAuditMessage {
+    private Long id;
+    private String corpId;
+    private Long seq;
+    private String msgId;
+    private Long msgTime;
+    private String msgType;
+
+    /**
+     * text.content
+     */
+    private String textContent;
+
+    /**
+     * 媒体通用字段(语音/图片/视频/表情等),字段名与表 media_sdkfileid / media_md5sum 及 Mapper 一致
+     */
+    private String mediaSdkfileid;
+    private String mediaMd5sum;
+    private Integer mediaSize;
+    private Integer mediaPlayLength;
+    private Integer mediaWidth;
+    private Integer mediaHeight;
+    private String mediaFileName;
+    private String mediaFileExt;
+
+    /**
+     * 媒体文件上传至 OSS 后的访问 URL(语音/视频)
+     */
+    private String mediaOssUrl;
+
+    /**
+     * 关联原始表
+     */
+    private Long rawId;
+
+    private Date createTime;
+}
+

+ 31 - 0
fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditRaw.java

@@ -0,0 +1,31 @@
+package com.fs.qw.domain.audit;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 企业微信会话存档原始明文(解密后) qw_msg_audit_raw
+ */
+@Data
+public class QwMsgAuditRaw {
+    private Long id;
+    private String corpId;
+    private Long seq;
+    private String msgId;
+    private String action;
+    private String fromUser;
+    /**
+     * 逗号分隔或 JSON 字符串(按现有项目风格存 String)
+     */
+    private String toList;
+    private String roomId;
+    private Long msgTime;
+    private String msgType;
+    /**
+     * 解密后的完整 JSON
+     */
+    private String rawJson;
+    private Date createTime;
+}
+

+ 21 - 0
fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditSeq.java

@@ -0,0 +1,21 @@
+package com.fs.qw.domain.audit;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 企业微信会话存档拉取进度(seq) qw_msg_audit_seq
+ */
+@Data
+public class QwMsgAuditSeq {
+    private Long id;
+    private String corpId;
+    /**
+     * 已成功处理到的最大 seq
+     */
+    private Long seq;
+    private Date createTime;
+    private Date updateTime;
+}
+

+ 75 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwMsgAuditMessageMapper.java

@@ -0,0 +1,75 @@
+package com.fs.qw.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * 企微会话存档结构化消息Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-19
+ */
+public interface QwMsgAuditMessageMapper extends BaseMapper<QwMsgAuditMessage>{
+    /**
+     * 查询企微会话存档结构化消息
+     *
+     * @param id 企微会话存档结构化消息主键
+     * @return 企微会话存档结构化消息
+     */
+    QwMsgAuditMessage selectQwMsgAuditMessageById(Long id);
+
+    /**
+     * 查询企微会话存档结构化消息列表
+     *
+     * @param qwMsgAuditMessage 企微会话存档结构化消息
+     * @return 企微会话存档结构化消息集合
+     */
+    List<QwMsgAuditMessage> selectQwMsgAuditMessageList(QwMsgAuditMessage qwMsgAuditMessage);
+
+    /**
+     * 新增企微会话存档结构化消息
+     *
+     * @param qwMsgAuditMessage 企微会话存档结构化消息
+     * @return 结果
+     */
+    int insertQwMsgAuditMessage(QwMsgAuditMessage qwMsgAuditMessage);
+
+    /**
+     * 修改企微会话存档结构化消息
+     *
+     * @param qwMsgAuditMessage 企微会话存档结构化消息
+     * @return 结果
+     */
+    int updateQwMsgAuditMessage(QwMsgAuditMessage qwMsgAuditMessage);
+
+    /**
+     * 删除企微会话存档结构化消息
+     *
+     * @param id 企微会话存档结构化消息主键
+     * @return 结果
+     */
+    int deleteQwMsgAuditMessageById(Long id);
+
+    /**
+     * 批量删除企微会话存档结构化消息
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteQwMsgAuditMessageByIds(Long[] ids);
+
+    @Select("select * from qw_msg_audit_message where corp_id = #{corpId} and seq = #{seq} limit 1")
+    QwMsgAuditMessage selectByCorpIdAndSeq(@Param("corpId") String corpId, @Param("seq") Long seq);
+
+    /**
+     * 查询指定企业下指定消息类型中 media_oss_url 为空的记录(待下载上传)
+     */
+    @Select("select * from qw_msg_audit_message where corp_id = #{corpId} and msg_type = #{msgType} and media_sdkfileid is not null and media_oss_url is null limit #{limit}")
+    List<QwMsgAuditMessage> selectMediaPendingOssUpload(@Param("corpId") String corpId, @Param("msgType") String msgType, @Param("limit") int limit);
+
+//    int batchInsertMessage(QwMsgAuditMessage record);
+
+}

+ 69 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwMsgAuditRawMapper.java

@@ -0,0 +1,69 @@
+package com.fs.qw.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.qw.domain.audit.QwMsgAuditRaw;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * 企微会话存档原始明文Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-19
+ */
+public interface QwMsgAuditRawMapper extends BaseMapper<QwMsgAuditRaw>{
+    /**
+     * 查询企微会话存档原始明文
+     *
+     * @param id 企微会话存档原始明文主键
+     * @return 企微会话存档原始明文
+     */
+    QwMsgAuditRaw selectQwMsgAuditRawById(Long id);
+
+    /**
+     * 查询企微会话存档原始明文列表
+     *
+     * @param qwMsgAuditRaw 企微会话存档原始明文
+     * @return 企微会话存档原始明文集合
+     */
+    List<QwMsgAuditRaw> selectQwMsgAuditRawList(QwMsgAuditRaw qwMsgAuditRaw);
+
+    /**
+     * 新增企微会话存档原始明文
+     *
+     * @param qwMsgAuditRaw 企微会话存档原始明文
+     * @return 结果
+     */
+    int insertQwMsgAuditRaw(QwMsgAuditRaw qwMsgAuditRaw);
+
+    /**
+     * 修改企微会话存档原始明文
+     *
+     * @param qwMsgAuditRaw 企微会话存档原始明文
+     * @return 结果
+     */
+    int updateQwMsgAuditRaw(QwMsgAuditRaw qwMsgAuditRaw);
+
+    /**
+     * 删除企微会话存档原始明文
+     *
+     * @param id 企微会话存档原始明文主键
+     * @return 结果
+     */
+    int deleteQwMsgAuditRawById(Long id);
+
+    /**
+     * 批量删除企微会话存档原始明文
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteQwMsgAuditRawByIds(Long[] ids);
+
+    @Select("select * from qw_msg_audit_raw where corp_id = #{corpId} and seq = #{seq} limit 1")
+    QwMsgAuditRaw selectByCorpIdAndSeq(@Param("corpId") String corpId, @Param("seq") Long seq);
+
+//    int batchInsertRaw(QwMsgAuditRaw record);
+
+}

+ 75 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwMsgAuditSeqMapper.java

@@ -0,0 +1,75 @@
+package com.fs.qw.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.qw.domain.audit.QwMsgAuditSeq;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+/**
+ * 企微会话存档拉取进度(seq)Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-19
+ */
+public interface QwMsgAuditSeqMapper extends BaseMapper<QwMsgAuditSeq>{
+    /**
+     * 查询企微会话存档拉取进度(seq)
+     *
+     * @param id 企微会话存档拉取进度(seq)主键
+     * @return 企微会话存档拉取进度(seq)
+     */
+    QwMsgAuditSeq selectQwMsgAuditSeqById(Long id);
+
+    /**
+     * 查询企微会话存档拉取进度(seq)列表
+     *
+     * @param qwMsgAuditSeq 企微会话存档拉取进度(seq)
+     * @return 企微会话存档拉取进度(seq)集合
+     */
+    List<QwMsgAuditSeq> selectQwMsgAuditSeqList(QwMsgAuditSeq qwMsgAuditSeq);
+
+    /**
+     * 新增企微会话存档拉取进度(seq)
+     *
+     * @param qwMsgAuditSeq 企微会话存档拉取进度(seq)
+     * @return 结果
+     */
+    int insertQwMsgAuditSeq(QwMsgAuditSeq qwMsgAuditSeq);
+
+    /**
+     * 修改企微会话存档拉取进度(seq)
+     *
+     * @param qwMsgAuditSeq 企微会话存档拉取进度(seq)
+     * @return 结果
+     */
+    int updateQwMsgAuditSeq(QwMsgAuditSeq qwMsgAuditSeq);
+
+    /**
+     * 删除企微会话存档拉取进度(seq)
+     *
+     * @param id 企微会话存档拉取进度(seq)主键
+     * @return 结果
+     */
+    int deleteQwMsgAuditSeqById(Long id);
+
+    /**
+     * 批量删除企微会话存档拉取进度(seq)
+     *
+     * @param ids 需要删除的数据主键集合
+     * @return 结果
+     */
+    int deleteQwMsgAuditSeqByIds(Long[] ids);
+
+    @Select("select * from qw_msg_audit_seq where corp_id = #{corpId} limit 1")
+    QwMsgAuditSeq selectByCorpId(@Param("corpId") String corpId);
+
+    /**
+     * 更新seq, 如果 seq 值小于等于 newSeq,则更新
+     * @param corpId 企微id
+     * @param newSeq 新的 seq
+     * @return
+     */
+    int advanceSeq(@Param("corpId") String corpId, @Param("newSeq") Long newSeq);
+
+}

+ 148 - 0
fs-service/src/main/resources/mapper/qw/QwMsgAuditMessageMapper.xml

@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.qw.mapper.QwMsgAuditMessageMapper">
+
+    <resultMap type="QwMsgAuditMessage" id="QwMsgAuditMessageResult">
+        <result property="id"    column="id"    />
+        <result property="corpId"    column="corp_id"    />
+        <result property="seq"    column="seq"    />
+        <result property="msgId"    column="msg_id"    />
+        <result property="msgTime"    column="msg_time"    />
+        <result property="msgType"    column="msg_type"    />
+        <result property="textContent"    column="text_content"    />
+        <result property="mediaSdkfileid"    column="media_sdkfileid"    />
+        <result property="mediaMd5sum"    column="media_md5sum"    />
+        <result property="mediaSize"    column="media_size"    />
+        <result property="mediaPlayLength"    column="media_play_length"    />
+        <result property="mediaWidth"    column="media_width"    />
+        <result property="mediaHeight"    column="media_height"    />
+        <result property="mediaFileName"    column="media_file_name"    />
+        <result property="mediaFileExt"    column="media_file_ext"    />
+        <result property="mediaOssUrl"    column="media_oss_url"    />
+        <result property="rawId"    column="raw_id"    />
+        <result property="createTime"    column="create_time"    />
+    </resultMap>
+
+    <sql id="selectQwMsgAuditMessageVo">
+        select id, corp_id, seq, msg_id, msg_time, msg_type, text_content, media_sdkfileid, media_md5sum, media_size, media_play_length, media_width, media_height, media_file_name, media_file_ext, media_oss_url, raw_id, create_time from qw_msg_audit_message
+    </sql>
+
+    <select id="selectQwMsgAuditMessageList" parameterType="QwMsgAuditMessage" resultMap="QwMsgAuditMessageResult">
+        <include refid="selectQwMsgAuditMessageVo"/>
+        <where>
+            <if test="corpId != null  and corpId != ''"> and corp_id = #{corpId}</if>
+            <if test="seq != null "> and seq = #{seq}</if>
+            <if test="msgId != null  and msgId != ''"> and msg_id = #{msgId}</if>
+            <if test="msgTime != null "> and msg_time = #{msgTime}</if>
+            <if test="msgType != null  and msgType != ''"> and msg_type = #{msgType}</if>
+            <if test="textContent != null  and textContent != ''"> and text_content = #{textContent}</if>
+            <if test="mediaSdkfileid != null  and mediaSdkfileid != ''"> and media_sdkfileid = #{mediaSdkfileid}</if>
+            <if test="mediaMd5sum != null  and mediaMd5sum != ''"> and media_md5sum = #{mediaMd5sum}</if>
+            <if test="mediaSize != null "> and media_size = #{mediaSize}</if>
+            <if test="mediaPlayLength != null "> and media_play_length = #{mediaPlayLength}</if>
+            <if test="mediaWidth != null "> and media_width = #{mediaWidth}</if>
+            <if test="mediaHeight != null "> and media_height = #{mediaHeight}</if>
+            <if test="mediaFileName != null  and mediaFileName != ''"> and media_file_name like concat('%', #{mediaFileName}, '%')</if>
+            <if test="mediaFileExt != null  and mediaFileExt != ''"> and media_file_ext = #{mediaFileExt}</if>
+            <if test="rawId != null "> and raw_id = #{rawId}</if>
+        </where>
+    </select>
+
+    <select id="selectQwMsgAuditMessageById" parameterType="Long" resultMap="QwMsgAuditMessageResult">
+        <include refid="selectQwMsgAuditMessageVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertQwMsgAuditMessage" parameterType="QwMsgAuditMessage" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_msg_audit_message
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="corpId != null and corpId != ''">corp_id,</if>
+            <if test="seq != null">seq,</if>
+            <if test="msgId != null">msg_id,</if>
+            <if test="msgTime != null">msg_time,</if>
+            <if test="msgType != null">msg_type,</if>
+            <if test="textContent != null">text_content,</if>
+            <if test="mediaSdkfileid != null">media_sdkfileid,</if>
+            <if test="mediaMd5sum != null">media_md5sum,</if>
+            <if test="mediaSize != null">media_size,</if>
+            <if test="mediaPlayLength != null">media_play_length,</if>
+            <if test="mediaWidth != null">media_width,</if>
+            <if test="mediaHeight != null">media_height,</if>
+            <if test="mediaFileName != null">media_file_name,</if>
+            <if test="mediaFileExt != null">media_file_ext,</if>
+            <if test="mediaOssUrl != null">media_oss_url,</if>
+            <if test="rawId != null">raw_id,</if>
+            <if test="createTime != null">create_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="corpId != null and corpId != ''">#{corpId},</if>
+            <if test="seq != null">#{seq},</if>
+            <if test="msgId != null">#{msgId},</if>
+            <if test="msgTime != null">#{msgTime},</if>
+            <if test="msgType != null">#{msgType},</if>
+            <if test="textContent != null">#{textContent},</if>
+            <if test="mediaSdkfileid != null">#{mediaSdkfileid},</if>
+            <if test="mediaMd5sum != null">#{mediaMd5sum},</if>
+            <if test="mediaSize != null">#{mediaSize},</if>
+            <if test="mediaPlayLength != null">#{mediaPlayLength},</if>
+            <if test="mediaWidth != null">#{mediaWidth},</if>
+            <if test="mediaHeight != null">#{mediaHeight},</if>
+            <if test="mediaFileName != null">#{mediaFileName},</if>
+            <if test="mediaFileExt != null">#{mediaFileExt},</if>
+            <if test="mediaOssUrl != null">#{mediaOssUrl},</if>
+            <if test="rawId != null">#{rawId},</if>
+            <if test="createTime != null">#{createTime},</if>
+         </trim>
+    </insert>
+
+    <update id="updateQwMsgAuditMessage" parameterType="QwMsgAuditMessage">
+        update qw_msg_audit_message
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="corpId != null and corpId != ''">corp_id = #{corpId},</if>
+            <if test="seq != null">seq = #{seq},</if>
+            <if test="msgId != null">msg_id = #{msgId},</if>
+            <if test="msgTime != null">msg_time = #{msgTime},</if>
+            <if test="msgType != null">msg_type = #{msgType},</if>
+            <if test="textContent != null">text_content = #{textContent},</if>
+            <if test="mediaSdkfileid != null">media_sdkfileid = #{mediaSdkfileid},</if>
+            <if test="mediaMd5sum != null">media_md5sum = #{mediaMd5sum},</if>
+            <if test="mediaSize != null">media_size = #{mediaSize},</if>
+            <if test="mediaPlayLength != null">media_play_length = #{mediaPlayLength},</if>
+            <if test="mediaWidth != null">media_width = #{mediaWidth},</if>
+            <if test="mediaHeight != null">media_height = #{mediaHeight},</if>
+            <if test="mediaFileName != null">media_file_name = #{mediaFileName},</if>
+            <if test="mediaFileExt != null">media_file_ext = #{mediaFileExt},</if>
+            <if test="mediaOssUrl != null">media_oss_url = #{mediaOssUrl},</if>
+            <if test="rawId != null">raw_id = #{rawId},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwMsgAuditMessageById" parameterType="Long">
+        delete from qw_msg_audit_message where id = #{id}
+    </delete>
+
+    <delete id="deleteQwMsgAuditMessageByIds" parameterType="String">
+        delete from qw_msg_audit_message where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+<!--    <insert id="batchInsertMessage" parameterType="com.fs.qw.domain.audit.QwMsgAuditMessage" useGeneratedKeys="true" keyProperty="id">-->
+<!--        insert into qw_msg_audit_message-->
+<!--        (corp_id, seq, msg_id, msg_time, msg_type, text_content,-->
+<!--         media_sdkfileid, media_md5sum, media_size, media_play_length,-->
+<!--         media_width, media_height, media_file_name, media_file_ext,-->
+<!--         raw_id, create_time)-->
+<!--        values-->
+<!--            (#{corpId}, #{seq}, #{msgId}, #{msgTime}, #{msgType}, #{textContent},-->
+<!--             #{mediaSdkfileid}, #{mediaMd5sum}, #{mediaSize}, #{mediaPlayLength},-->
+<!--             #{mediaWidth}, #{mediaHeight}, #{mediaFileName}, #{mediaFileExt},-->
+<!--             #{rawId}, #{createTime})-->
+<!--    </insert>-->
+
+</mapper>

+ 113 - 0
fs-service/src/main/resources/mapper/qw/QwMsgAuditRawMapper.xml

@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.qw.mapper.QwMsgAuditRawMapper">
+
+    <resultMap type="QwMsgAuditRaw" id="QwMsgAuditRawResult">
+        <result property="id"    column="id"    />
+        <result property="corpId"    column="corp_id"    />
+        <result property="seq"    column="seq"    />
+        <result property="msgId"    column="msg_id"    />
+        <result property="action"    column="action"    />
+        <result property="fromUser"    column="from_user"    />
+        <result property="toList"    column="to_list"    />
+        <result property="roomId"    column="room_id"    />
+        <result property="msgTime"    column="msg_time"    />
+        <result property="msgType"    column="msg_type"    />
+        <result property="rawJson"    column="raw_json"    />
+        <result property="createTime"    column="create_time"    />
+    </resultMap>
+
+    <sql id="selectQwMsgAuditRawVo">
+        select id, corp_id, seq, msg_id, action, from_user, to_list, room_id, msg_time, msg_type, raw_json, create_time from qw_msg_audit_raw
+    </sql>
+
+    <select id="selectQwMsgAuditRawList" parameterType="QwMsgAuditRaw" resultMap="QwMsgAuditRawResult">
+        <include refid="selectQwMsgAuditRawVo"/>
+        <where>
+            <if test="corpId != null  and corpId != ''"> and corp_id = #{corpId}</if>
+            <if test="seq != null "> and seq = #{seq}</if>
+            <if test="msgId != null  and msgId != ''"> and msg_id = #{msgId}</if>
+            <if test="action != null  and action != ''"> and action = #{action}</if>
+            <if test="fromUser != null  and fromUser != ''"> and from_user = #{fromUser}</if>
+            <if test="toList != null  and toList != ''"> and to_list = #{toList}</if>
+            <if test="roomId != null  and roomId != ''"> and room_id = #{roomId}</if>
+            <if test="msgTime != null "> and msg_time = #{msgTime}</if>
+            <if test="msgType != null  and msgType != ''"> and msg_type = #{msgType}</if>
+            <if test="rawJson != null  and rawJson != ''"> and raw_json = #{rawJson}</if>
+        </where>
+    </select>
+
+    <select id="selectQwMsgAuditRawById" parameterType="Long" resultMap="QwMsgAuditRawResult">
+        <include refid="selectQwMsgAuditRawVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertQwMsgAuditRaw" parameterType="QwMsgAuditRaw" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_msg_audit_raw
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="corpId != null and corpId != ''">corp_id,</if>
+            <if test="seq != null">seq,</if>
+            <if test="msgId != null">msg_id,</if>
+            <if test="action != null">action,</if>
+            <if test="fromUser != null">from_user,</if>
+            <if test="toList != null">to_list,</if>
+            <if test="roomId != null">room_id,</if>
+            <if test="msgTime != null">msg_time,</if>
+            <if test="msgType != null">msg_type,</if>
+            <if test="rawJson != null and rawJson != ''">raw_json,</if>
+            <if test="createTime != null">create_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="corpId != null and corpId != ''">#{corpId},</if>
+            <if test="seq != null">#{seq},</if>
+            <if test="msgId != null">#{msgId},</if>
+            <if test="action != null">#{action},</if>
+            <if test="fromUser != null">#{fromUser},</if>
+            <if test="toList != null">#{toList},</if>
+            <if test="roomId != null">#{roomId},</if>
+            <if test="msgTime != null">#{msgTime},</if>
+            <if test="msgType != null">#{msgType},</if>
+            <if test="rawJson != null and rawJson != ''">#{rawJson},</if>
+            <if test="createTime != null">#{createTime},</if>
+         </trim>
+    </insert>
+
+    <update id="updateQwMsgAuditRaw" parameterType="QwMsgAuditRaw">
+        update qw_msg_audit_raw
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="corpId != null and corpId != ''">corp_id = #{corpId},</if>
+            <if test="seq != null">seq = #{seq},</if>
+            <if test="msgId != null">msg_id = #{msgId},</if>
+            <if test="action != null">action = #{action},</if>
+            <if test="fromUser != null">from_user = #{fromUser},</if>
+            <if test="toList != null">to_list = #{toList},</if>
+            <if test="roomId != null">room_id = #{roomId},</if>
+            <if test="msgTime != null">msg_time = #{msgTime},</if>
+            <if test="msgType != null">msg_type = #{msgType},</if>
+            <if test="rawJson != null and rawJson != ''">raw_json = #{rawJson},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwMsgAuditRawById" parameterType="Long">
+        delete from qw_msg_audit_raw where id = #{id}
+    </delete>
+
+    <delete id="deleteQwMsgAuditRawByIds" parameterType="String">
+        delete from qw_msg_audit_raw where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+<!--    <insert id="batchInsertRaw" parameterType="com.fs.qw.domain.audit.QwMsgAuditRaw" useGeneratedKeys="true" keyProperty="id">-->
+<!--        insert into qw_msg_audit_raw-->
+<!--        (corp_id, seq, msg_id, action, from_user, to_list, room_id, msg_time, msg_type, raw_json, create_time)-->
+<!--        values-->
+<!--            (#{corpId}, #{seq}, #{msgId}, #{action}, #{fromUser}, #{toList}, #{roomId}, #{msgTime}, #{msgType}, #{rawJson}, #{createTime})-->
+<!--    </insert>-->
+
+</mapper>

+ 78 - 0
fs-service/src/main/resources/mapper/qw/QwMsgAuditSeqMapper.xml

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.qw.mapper.QwMsgAuditSeqMapper">
+
+    <resultMap type="QwMsgAuditSeq" id="QwMsgAuditSeqResult">
+        <result property="id"    column="id"    />
+        <result property="corpId"    column="corp_id"    />
+        <result property="seq"    column="seq"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="updateTime"    column="update_time"    />
+    </resultMap>
+
+    <sql id="selectQwMsgAuditSeqVo">
+        select id, corp_id, seq, create_time, update_time from qw_msg_audit_seq
+    </sql>
+
+    <select id="selectQwMsgAuditSeqList" parameterType="QwMsgAuditSeq" resultMap="QwMsgAuditSeqResult">
+        <include refid="selectQwMsgAuditSeqVo"/>
+        <where>
+            <if test="corpId != null  and corpId != ''"> and corp_id = #{corpId}</if>
+            <if test="seq != null "> and seq = #{seq}</if>
+        </where>
+    </select>
+
+    <select id="selectQwMsgAuditSeqById" parameterType="Long" resultMap="QwMsgAuditSeqResult">
+        <include refid="selectQwMsgAuditSeqVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertQwMsgAuditSeq" parameterType="QwMsgAuditSeq" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_msg_audit_seq
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="corpId != null and corpId != ''">corp_id,</if>
+            <if test="seq != null">seq,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="updateTime != null">update_time,</if>
+         </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="corpId != null and corpId != ''">#{corpId},</if>
+            <if test="seq != null">#{seq},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+         </trim>
+    </insert>
+
+    <update id="updateQwMsgAuditSeq" parameterType="QwMsgAuditSeq">
+        update qw_msg_audit_seq
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="corpId != null and corpId != ''">corp_id = #{corpId},</if>
+            <if test="seq != null">seq = #{seq},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwMsgAuditSeqById" parameterType="Long">
+        delete from qw_msg_audit_seq where id = #{id}
+    </delete>
+
+    <delete id="deleteQwMsgAuditSeqByIds" parameterType="String">
+        delete from qw_msg_audit_seq where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <update id="advanceSeq">
+        update qw_msg_audit_seq
+        set seq = #{newSeq},
+            update_time = now()
+        where corp_id = #{corpId}
+          and (seq is null or seq &lt;= #{newSeq})
+    </update>
+
+</mapper>