更改
This commit is contained in:
parent
acb8a77c53
commit
cba4790374
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -8,7 +8,7 @@
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="15" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="15" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
6
pom.xml
6
pom.xml
@ -55,6 +55,12 @@
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.5.6</version> <!-- 推荐使用最新稳定版 -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.aliyun.oss</groupId>
|
||||
<artifactId>aliyun-sdk-oss</artifactId>
|
||||
<version>3.17.4</version> <!-- 推荐使用最新版 -->
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
|
||||
@ -74,7 +74,7 @@ public class Finance {
|
||||
*/
|
||||
public native static int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg);
|
||||
|
||||
public native static void DestroySdk(long sdk);
|
||||
public native static int DestroySdk(long sdk);
|
||||
|
||||
public native static long NewSlice();
|
||||
|
||||
|
||||
@ -1,45 +1,66 @@
|
||||
package org.shop.crop;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.tencent.wework.Finance;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.shop.crop.constant.GlobalConstants;
|
||||
import org.shop.crop.dao.CropConfig;
|
||||
import org.shop.crop.dao.CropFile;
|
||||
import org.shop.crop.dao.CropMessage;
|
||||
import org.shop.crop.dto.BaseMessageDto;
|
||||
import org.shop.crop.dto.ChatData;
|
||||
import org.shop.crop.dto.Message;
|
||||
import org.shop.crop.dto.MessagePullResponse;
|
||||
import org.shop.crop.mapper.CropConfigMapper;
|
||||
import org.shop.crop.service.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.DefaultTransactionDefinition;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
public class CropController {
|
||||
|
||||
@Autowired
|
||||
Long sdk;
|
||||
FinanceSdkManager sdkManager;
|
||||
|
||||
@Autowired
|
||||
private Environment environment; // nul
|
||||
CropMessageService cropMessageService;
|
||||
|
||||
@Autowired
|
||||
CropFileService cropFileService;
|
||||
|
||||
@Autowired
|
||||
CropConfigMapper cropConfigMapper;
|
||||
|
||||
@Autowired
|
||||
MediaContext mediaContext;
|
||||
|
||||
@Autowired
|
||||
OssService ossService;
|
||||
|
||||
@GetMapping("/")
|
||||
public String index() {
|
||||
return "Hello, World!";
|
||||
}
|
||||
|
||||
@GetMapping("/crop/chatData")
|
||||
private List<Message> chatData(Long startSeq) throws IOException {
|
||||
public List<CropMessage> chatData(Long startSeq,String corpId) throws IOException {
|
||||
long limit = 1000;
|
||||
long slice = Finance.NewSlice();
|
||||
Long sdk = sdkManager.initSdk(corpId, "mEizahrSF6axdfWtSK_f73a3j6-sV02hhyGG7ogmTpM");
|
||||
|
||||
int ret = Finance.GetChatData(sdk, startSeq, limit, "", "", 1000, slice);
|
||||
if (ret != 0) {
|
||||
System.out.println("调用sdk拉取消息接口失败,失败消息为 ret = " + ret);
|
||||
@ -48,7 +69,7 @@ public class CropController {
|
||||
}
|
||||
String contentResult = Finance.GetContentFromSlice(slice);
|
||||
log.info("内容字符串" + contentResult);
|
||||
return decodeChatData(contentResult);
|
||||
return decodeChatData(contentResult,startSeq,sdk,corpId);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -56,9 +77,9 @@ public class CropController {
|
||||
*
|
||||
* @param contentResult 拉取到的JSON原文
|
||||
*/
|
||||
public List<Message> decodeChatData(String contentResult) throws IOException {
|
||||
public List<CropMessage> decodeChatData(String contentResult,Long startSeq,Long sdk,String corpId) throws IOException {
|
||||
// 1.基础信息解析
|
||||
List<Message> messageList = new ArrayList<>();
|
||||
List<CropMessage> messageList = new ArrayList<>();
|
||||
MessagePullResponse messagePullResponse = JSON.parseObject(contentResult, MessagePullResponse.class);
|
||||
if (messagePullResponse == null || messagePullResponse.getChatdata() == null) {
|
||||
return Collections.emptyList();
|
||||
@ -70,27 +91,203 @@ public class CropController {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String privateKey = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
|
||||
// 3. 提取所有 seq
|
||||
log.info("chatdataList={} 加密数据",chatdataList);
|
||||
// 4. 查询数据库中已存在的 seq
|
||||
// 直接在业务代码中(或封装到 service)
|
||||
List<Long> existingSeqs = cropMessageService.lambdaQuery()
|
||||
.gt(CropMessage::getSeq, startSeq)
|
||||
.select(CropMessage::getSeq)
|
||||
.list()
|
||||
.stream()
|
||||
.map(CropMessage::getSeq)
|
||||
.collect(Collectors.toList());
|
||||
Set<Long> existingSeqSet = new HashSet<>(existingSeqs);
|
||||
log.info("existingSeqSet={} 加密数据",existingSeqSet);
|
||||
|
||||
CropConfig config=cropConfigMapper.selectById(1);
|
||||
Long maxSeq = config.getLastSeq();
|
||||
// 改为:
|
||||
List<CropFile> mediaFileInfos = new ArrayList<>();
|
||||
for (ChatData chatData : chatdataList) {
|
||||
// ✅ 如果 seq 已存在,跳过
|
||||
if (existingSeqSet.contains(chatData.getSeq())) {
|
||||
log.debug("seq={} 已存在于数据库,跳过解密和插入", chatData.getSeq());
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
// 解密ChatData数据
|
||||
String plainTextJson = decrypt(chatData, privateKey);
|
||||
String plainTextJson = decrypt(chatData, privateKey,sdk);
|
||||
BaseMessageDto messageDto = JSON.parseObject(plainTextJson, BaseMessageDto.class);
|
||||
// 转换dto数据到entity
|
||||
Message message = messageDto.convertToMessage();
|
||||
CropMessage message = messageDto.convertToMessage();
|
||||
message.setCropId(corpId);
|
||||
message.setSeq(chatData.getSeq());
|
||||
message.setOriginContent(plainTextJson);
|
||||
message.setContent(plainTextJson);
|
||||
messageList.add(message);
|
||||
// ✅ 提取 sdkfileid
|
||||
List<CropFile> files = extractMediaFiles(plainTextJson, chatData.getSeq());
|
||||
mediaFileInfos.addAll(files); // 收集所有待下载文件
|
||||
|
||||
if (chatData.getSeq() > maxSeq) {
|
||||
maxSeq = chatData.getSeq();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解密失败{}", e.getMessage());
|
||||
// 收集失败情况数据
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return saveData(messageList,maxSeq,config,mediaFileInfos,sdk,corpId);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 异步下载待处理的媒体文件
|
||||
*
|
||||
* @return List<CropMessage>
|
||||
*/
|
||||
@Transactional
|
||||
public List<CropMessage> saveData(List<CropMessage> messageList, Long maxSeq, CropConfig config
|
||||
, List<CropFile>mediaFileInfos,Long sdk,String corpId) {
|
||||
|
||||
// 4.保存消息和失败任务
|
||||
cropMessageService.saveBatch(messageList);
|
||||
config.setLastSeq(maxSeq);
|
||||
cropConfigMapper.updateById(config);
|
||||
|
||||
if (!mediaFileInfos.isEmpty()) {
|
||||
List<CropFile> mediaFiles = mediaFileInfos.stream().map(info -> {
|
||||
CropFile mf = new CropFile();
|
||||
mf.setSdkFileId(info.getSdkFileId());
|
||||
mf.setMsgType(info.getMsgType());
|
||||
mf.setSeq(info.getSeq());
|
||||
mf.setCropId(corpId);
|
||||
mf.setStatus(0);
|
||||
return mf;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
// 保存到数据库
|
||||
cropFileService.saveBatch(mediaFiles);
|
||||
|
||||
// ✅ 异步触发下载任务(可以轮询数据库)
|
||||
CompletableFuture.runAsync(() -> {
|
||||
downloadPendingFiles(mediaFiles,sdk); // 也可以不传参,改为查数据库
|
||||
});
|
||||
}
|
||||
return messageList;
|
||||
}
|
||||
/**
|
||||
* 异步下载待处理的媒体文件
|
||||
*/
|
||||
private void downloadPendingFiles(List<CropFile> mediaFiles,Long sdk) {
|
||||
for (CropFile mf : mediaFiles) {
|
||||
try {
|
||||
String url = downFile(mf.getSdkFileId(), mf.getMsgType(),sdk);
|
||||
if (url != null && !url.isEmpty()) {
|
||||
mf.setUrl(url);
|
||||
mf.setStatus(1);
|
||||
} else {
|
||||
mf.setStatus(2);
|
||||
mf.setErrorMsg("下载或上传失败");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("下载文件失败 sdkfileid={}", mf.getSdkFileId(), e);
|
||||
mf.setStatus(2);
|
||||
mf.setErrorMsg(e.getMessage().substring(0, Math.min(e.getMessage().length(), 500)));
|
||||
} finally {
|
||||
// 更新数据库状态
|
||||
cropFileService.updateById(mf);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 提取所有需要下载的媒体文件信息
|
||||
*/
|
||||
private List<CropFile> extractMediaFiles(String plainTextJson, Long seq) {
|
||||
List<CropFile> result = new ArrayList<>();
|
||||
try {
|
||||
JSONObject root = JSONObject.parseObject(plainTextJson);
|
||||
|
||||
// ===== 区分是单个消息 还是 msglist 数组 =====
|
||||
JSONArray msgList;
|
||||
|
||||
if (root.containsKey("msglist") && root.get("msglist") instanceof JSONArray) {
|
||||
// 格式1: { "msglist": [ {...}, {...} ] }
|
||||
msgList = root.getJSONArray("msglist");
|
||||
} else {
|
||||
// 格式2: { "msgid": "...", "msgtype": "image", "image": { ... } }
|
||||
// 把单个消息包装成数组
|
||||
msgList = new JSONArray();
|
||||
msgList.add(root);
|
||||
}
|
||||
|
||||
for (int i = 0; i < msgList.size(); i++) {
|
||||
JSONObject msg = msgList.getJSONObject(i);
|
||||
String msgType = msg.getString("msgtype");
|
||||
|
||||
if (Arrays.asList("image", "voice", "video", "file").contains(msgType)) {
|
||||
if (msg.containsKey(msgType)) {
|
||||
String sdkFileId = msg.getJSONObject(msgType).getString("sdkfileid");
|
||||
if (sdkFileId != null && !sdkFileId.trim().isEmpty()) {
|
||||
CropFile cropFile = new CropFile(); // 注意:你写成了 CropConfig,应为 CropFile
|
||||
cropFile.setMsgType(msgType);
|
||||
cropFile.setSeq(seq);
|
||||
cropFile.setSdkFileId(sdkFileId);
|
||||
result.add(cropFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("提取媒体文件失败 seq={}: {}", seq, e.getMessage(), e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@GetMapping("/crop/downFile")
|
||||
public String downFile(String sdkField,String msgType,Long sdk) throws IOException {
|
||||
File tempFile = File.createTempFile(GlobalConstants.DOWNLOAD_TEMP_FILE_NAME_PREFIX ,
|
||||
getSuffixByMsgType(msgType), mediaContext.getTempMergeFileDir());
|
||||
String indexbuf = null; // 初始化 indexbuf
|
||||
try (FileOutputStream fileOutputStream = new FileOutputStream(tempFile)) {
|
||||
while (true) {
|
||||
long mediaData = Finance.NewMediaData();
|
||||
int ret = Finance.GetMediaData(sdk, indexbuf, sdkField, null, null, 3, mediaData);
|
||||
if (ret != 0) {
|
||||
Finance.FreeMediaData(mediaData);
|
||||
tempFile.delete();
|
||||
break;
|
||||
}
|
||||
System.out.println(Finance.GetOutIndexBuf(mediaData));
|
||||
fileOutputStream.write(Finance.GetData(mediaData));
|
||||
if (Finance.IsMediaDataFinish(mediaData) == 1) {
|
||||
Finance.FreeMediaData(mediaData);
|
||||
return ossService.upload(tempFile);
|
||||
} else {
|
||||
indexbuf = Finance.GetOutIndexBuf(mediaData);
|
||||
Finance.FreeMediaData(mediaData);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getSuffixByMsgType(String msgType) {
|
||||
switch (msgType) {
|
||||
case "image":
|
||||
return ".jpg";
|
||||
case "voice":
|
||||
return ".amr";
|
||||
case "video":
|
||||
return ".mp4";
|
||||
case "file":
|
||||
return ".bin";
|
||||
default:
|
||||
return ".bin";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解密数据
|
||||
@ -99,7 +296,7 @@ public class CropController {
|
||||
* @param privateKeyStr 私钥字符串
|
||||
* @return 解密后的原文
|
||||
*/
|
||||
private String decrypt(ChatData chatData, String privateKeyStr) throws Exception {
|
||||
private String decrypt(ChatData chatData, String privateKeyStr,Long sdk) throws Exception {
|
||||
// 1.解密EncryptRandomKey
|
||||
String randomKey = EncodeUtils.decryptRandomKey(chatData.getEncryptRandomKey(), privateKeyStr);
|
||||
// 2.调用SDK方法解密密文数据
|
||||
|
||||
@ -7,7 +7,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
@Configuration
|
||||
public class MyConfig {
|
||||
|
||||
@Bean
|
||||
/* @Bean
|
||||
public long sdk() {
|
||||
System.out.println("Initializing SDK...");
|
||||
long sdk = Finance.NewSdk();
|
||||
@ -22,5 +22,5 @@ public class MyConfig {
|
||||
throw new RuntimeException("初始化sdk失败,失败消息为:ret = " + init);
|
||||
}
|
||||
return sdk;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
12
src/main/java/org/shop/crop/config/MediaProperties.java
Normal file
12
src/main/java/org/shop/crop/config/MediaProperties.java
Normal file
@ -0,0 +1,12 @@
|
||||
package org.shop.crop.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "wechat.media")
|
||||
@Data
|
||||
public class MediaProperties {
|
||||
private String tempDir;
|
||||
}
|
||||
13
src/main/java/org/shop/crop/constant/GlobalConstants.java
Normal file
13
src/main/java/org/shop/crop/constant/GlobalConstants.java
Normal file
@ -0,0 +1,13 @@
|
||||
package org.shop.crop.constant;
|
||||
|
||||
public class GlobalConstants {
|
||||
/**
|
||||
* 临时文件名前缀
|
||||
*/
|
||||
public static final String DOWNLOAD_TEMP_FILE_NAME_PREFIX = "wx_media_";
|
||||
|
||||
/**
|
||||
* 临时文件名后缀(注意:以 . 开头)
|
||||
*/
|
||||
public static final String TEMP_FILE_NAME_SUFFIX = ".tmp"; // 可以是 .dat, .bin 等
|
||||
}
|
||||
49
src/main/java/org/shop/crop/dao/CropConfig.java
Normal file
49
src/main/java/org/shop/crop/dao/CropConfig.java
Normal file
@ -0,0 +1,49 @@
|
||||
package org.shop.crop.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@Data
|
||||
@TableName("crop_configs")
|
||||
public class CropConfig {
|
||||
/**
|
||||
* 主键 ID(自增)
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 主键 ID(自增)
|
||||
*/
|
||||
@TableField(value = "cropid")
|
||||
private String cropid;
|
||||
|
||||
/**
|
||||
* 上次的点位
|
||||
*/
|
||||
@TableField(value = "last_seq")
|
||||
private Long lastSeq;
|
||||
|
||||
/**
|
||||
* 秘钥
|
||||
*/
|
||||
@TableField("message_secret")
|
||||
private String messageSecret;
|
||||
/**
|
||||
* 创建时间(备用字段,可选)
|
||||
*/
|
||||
@TableField("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 修改时间(自动填充)
|
||||
*/
|
||||
@TableField("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
|
||||
}
|
||||
37
src/main/java/org/shop/crop/dao/CropFile.java
Normal file
37
src/main/java/org/shop/crop/dao/CropFile.java
Normal file
@ -0,0 +1,37 @@
|
||||
package org.shop.crop.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("crop_files")
|
||||
public class CropFile {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField(value = "cropid")
|
||||
private String cropId;
|
||||
|
||||
private String sdkFileId; // 企微返回的 sdkfileid
|
||||
|
||||
private String msgType; // image/voice/video/file
|
||||
|
||||
private Long seq; // 对应消息的 seq
|
||||
|
||||
private String url; // 下载后上传 OSS 的地址
|
||||
|
||||
private String fileName; // 可选:原始文件名(后续可补充)
|
||||
|
||||
private Integer status = 0; // 0 pending, 1 success, 2 failed
|
||||
|
||||
private String errorMsg; // 失败原因
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@ -12,10 +12,9 @@ import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Data
|
||||
@TableName("crop_message")
|
||||
@TableName("crop_messages")
|
||||
public class CropMessage {
|
||||
|
||||
|
||||
/**
|
||||
* 主键 ID(自增)
|
||||
*/
|
||||
@ -28,6 +27,12 @@ public class CropMessage {
|
||||
@TableField("msg_id")
|
||||
private String msgId;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@TableField("cropid")
|
||||
private String cropId;
|
||||
|
||||
/**
|
||||
* 消息动作(如 send, reply, delete)
|
||||
*/
|
||||
@ -37,7 +42,7 @@ public class CropMessage {
|
||||
/**
|
||||
* 发送方 ID
|
||||
*/
|
||||
@TableField("from")
|
||||
@TableField("from_id")
|
||||
private String from; // 注意:'from' 是关键字,不能直接写,所以用注解指定
|
||||
|
||||
/**
|
||||
@ -46,6 +51,12 @@ public class CropMessage {
|
||||
@TableField("tolist")
|
||||
private String tolist;
|
||||
|
||||
/**
|
||||
* 接收方列表(JSON 或逗号分隔)
|
||||
*/
|
||||
@TableField("to_id")
|
||||
private String toId;
|
||||
|
||||
/**
|
||||
* 群聊消息的房间 ID
|
||||
*/
|
||||
@ -74,7 +85,7 @@ public class CropMessage {
|
||||
* 消息发送时间
|
||||
*/
|
||||
@TableField("msgtime")
|
||||
private LocalDateTime msgtime;
|
||||
private String msgtime;
|
||||
|
||||
/**
|
||||
* 消息状态(是否已处理)
|
||||
@ -82,12 +93,6 @@ public class CropMessage {
|
||||
@TableField("status")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 创建时间(自动填充)
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
/**
|
||||
* 逻辑删除标志(0:正常,1:已删除)
|
||||
*/
|
||||
@ -98,14 +103,14 @@ public class CropMessage {
|
||||
/**
|
||||
* 创建时间(备用字段,可选)
|
||||
*/
|
||||
@TableField("gmt_created")
|
||||
private LocalDateTime gmtCreated;
|
||||
@TableField("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 修改时间(自动填充)
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime gmtModified;
|
||||
@TableField("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import org.shop.crop.dao.CropMessage;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
@ -33,7 +34,7 @@ public class BaseMessageDto {
|
||||
* 消息发送方id
|
||||
*/
|
||||
@JSONField(name = "from")
|
||||
private String fromId;
|
||||
private String from;
|
||||
|
||||
/**
|
||||
* 消息接收方列表
|
||||
@ -62,25 +63,26 @@ public class BaseMessageDto {
|
||||
|
||||
// getter setter
|
||||
|
||||
public Message convertToMessage(){
|
||||
Message message = new Message();
|
||||
public CropMessage convertToMessage(){
|
||||
CropMessage message = new CropMessage();
|
||||
message.setMsgId(this.msgId);
|
||||
message.setAction(this.action);
|
||||
message.setFromId(this.fromId);
|
||||
message.setMsgTime(this.msgTime);
|
||||
message.setMsgType(this.msgType);
|
||||
message.setRoomId(this.roomId);
|
||||
message.setFrom(this.from);
|
||||
message.setMsgtype(this.msgType);
|
||||
message.setRoomid(this.roomId);
|
||||
// id数组转为字符串
|
||||
message.setToList(JSON.toJSONString(this.toList));
|
||||
message.setTolist(JSON.toJSONString(this.toList));
|
||||
message.setToId(this.toList.get(0));
|
||||
// 格式化时间
|
||||
if(this.getMsgTime() != null){
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");;
|
||||
long seconds = this.getMsgTime() / 1000;
|
||||
LocalDateTime dateTime = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochSecond(this.getMsgTime()),
|
||||
Instant.ofEpochSecond(seconds),
|
||||
ZoneId.systemDefault()
|
||||
);
|
||||
String timeStr=dateTime.format(formatter);
|
||||
message.setMsgTimeStr(timeStr);
|
||||
message.setMsgtime(timeStr);
|
||||
}
|
||||
|
||||
|
||||
|
||||
12
src/main/java/org/shop/crop/mapper/CropConfigMapper.java
Normal file
12
src/main/java/org/shop/crop/mapper/CropConfigMapper.java
Normal file
@ -0,0 +1,12 @@
|
||||
package org.shop.crop.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.shop.crop.dao.CropConfig;
|
||||
import org.shop.crop.dao.CropMessage;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface CropConfigMapper extends BaseMapper<CropConfig> {
|
||||
}
|
||||
12
src/main/java/org/shop/crop/mapper/CropFileMapper.java
Normal file
12
src/main/java/org/shop/crop/mapper/CropFileMapper.java
Normal file
@ -0,0 +1,12 @@
|
||||
package org.shop.crop.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.shop.crop.dao.CropConfig;
|
||||
import org.shop.crop.dao.CropFile;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Mapper
|
||||
public interface CropFileMapper extends BaseMapper<CropFile> {
|
||||
}
|
||||
11
src/main/java/org/shop/crop/service/CropFileService.java
Normal file
11
src/main/java/org/shop/crop/service/CropFileService.java
Normal file
@ -0,0 +1,11 @@
|
||||
package org.shop.crop.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.shop.crop.dao.CropFile;
|
||||
import org.shop.crop.dao.CropMessage;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public interface CropFileService extends IService<CropFile> {
|
||||
|
||||
}
|
||||
17
src/main/java/org/shop/crop/service/CropFileServiceImpl.java
Normal file
17
src/main/java/org/shop/crop/service/CropFileServiceImpl.java
Normal file
@ -0,0 +1,17 @@
|
||||
package org.shop.crop.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.shop.crop.dao.CropFile;
|
||||
import org.shop.crop.dao.CropMessage;
|
||||
import org.shop.crop.mapper.CropFileMapper;
|
||||
import org.shop.crop.mapper.CropMessageMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class CropFileServiceImpl extends ServiceImpl<CropFileMapper, CropFile>
|
||||
implements CropFileService{
|
||||
|
||||
@Autowired
|
||||
private CropFileMapper cropFileMapper;
|
||||
}
|
||||
13
src/main/java/org/shop/crop/service/CropMessageService.java
Normal file
13
src/main/java/org/shop/crop/service/CropMessageService.java
Normal file
@ -0,0 +1,13 @@
|
||||
package org.shop.crop.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.shop.crop.dao.CropMessage;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public interface CropMessageService extends IService<CropMessage> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package org.shop.crop.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.shop.crop.dao.CropMessage;
|
||||
import org.shop.crop.mapper.CropMessageMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class CropMessageServiceImpl extends ServiceImpl<CropMessageMapper, CropMessage>
|
||||
implements CropMessageService{
|
||||
|
||||
@Autowired
|
||||
private CropMessageMapper cropMessageMapper;
|
||||
|
||||
|
||||
}
|
||||
68
src/main/java/org/shop/crop/service/FinanceSdkManager.java
Normal file
68
src/main/java/org/shop/crop/service/FinanceSdkManager.java
Normal file
@ -0,0 +1,68 @@
|
||||
package org.shop.crop.service;
|
||||
|
||||
import com.tencent.wework.Finance;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class FinanceSdkManager {
|
||||
|
||||
// 存储所有已初始化的 SDK 实例:corpId -> sdk handle
|
||||
private final Map<String, Long> sdkMap = new ConcurrentHashMap<>();
|
||||
|
||||
// 存储每个企业的 secret 配置
|
||||
private final Map<String, String> secretMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 初始化一个企业的 SDK 客户端
|
||||
*/
|
||||
public synchronized Long initSdk(String corpId, String secret) {
|
||||
if (sdkMap.containsKey(corpId)) {
|
||||
return sdkMap.get(corpId); // 已存在,直接返回
|
||||
}
|
||||
|
||||
long sdk = Finance.NewSdk();
|
||||
int init = Finance.Init(
|
||||
sdk,
|
||||
corpId,
|
||||
secret
|
||||
);
|
||||
if (init != 0) {
|
||||
Finance.DestroySdk(sdk);
|
||||
throw new RuntimeException("初始化sdk失败,失败消息为:ret = " + init);
|
||||
}
|
||||
|
||||
sdkMap.put(corpId, sdk);
|
||||
secretMap.put(corpId, secret);
|
||||
|
||||
System.out.println("✅ SDK initialized for corpId: " + corpId + ", sdk=" + sdk);
|
||||
return sdk;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个企业的 SDK 实例
|
||||
*/
|
||||
public Long getSdk(String corpId) {
|
||||
Long sdk = sdkMap.get(corpId);
|
||||
if (sdk == null) {
|
||||
throw new IllegalArgumentException("No SDK initialized for corpId: " + corpId);
|
||||
}
|
||||
return sdk;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁某个企业的 SDK 实例
|
||||
*/
|
||||
public synchronized void destroySdk(String corpId) {
|
||||
Long sdk = sdkMap.remove(corpId);
|
||||
if (sdk != null && sdk > 0) {
|
||||
int ret = Finance.DestroySdk(sdk); // 或 Finance.FreeSdk(sdk)
|
||||
System.out.println("🔥 SDK destroyed for corpId: " + corpId + ", ret=" + ret);
|
||||
}
|
||||
secretMap.remove(corpId);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
33
src/main/java/org/shop/crop/service/MediaContext.java
Normal file
33
src/main/java/org/shop/crop/service/MediaContext.java
Normal file
@ -0,0 +1,33 @@
|
||||
package org.shop.crop.service;
|
||||
|
||||
import org.shop.crop.config.MediaProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@Component
|
||||
public class MediaContext {
|
||||
|
||||
private final File tempMergeFileDir;
|
||||
|
||||
public MediaContext(MediaProperties mediaProperties) {
|
||||
this.tempMergeFileDir = createTempDir(mediaProperties.getTempDir());
|
||||
}
|
||||
|
||||
private File createTempDir(String path) {
|
||||
File dir = new File(path);
|
||||
if (!dir.exists()) {
|
||||
boolean created = dir.mkdirs();
|
||||
if (!created) {
|
||||
throw new RuntimeException("无法创建临时目录: " + path);
|
||||
}
|
||||
System.out.println("✅ 已创建临时目录: " + path);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
public File getTempMergeFileDir() {
|
||||
return tempMergeFileDir;
|
||||
}
|
||||
}
|
||||
|
||||
142
src/main/java/org/shop/crop/service/OssService.java
Normal file
142
src/main/java/org/shop/crop/service/OssService.java
Normal file
@ -0,0 +1,142 @@
|
||||
package org.shop.crop.service;
|
||||
|
||||
import com.aliyun.oss.OSS;
|
||||
import com.aliyun.oss.OSSClientBuilder;
|
||||
import com.aliyun.oss.model.OSSObject;
|
||||
import com.aliyun.oss.model.ObjectMetadata;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OssService {
|
||||
|
||||
private static final String ENDPOINT = "https://oss-cn-hangzhou.aliyuncs.com"; // 你 bucket 的区域
|
||||
private static final String ACCESS_KEY_ID = "LTAI6do39W9jINpe";
|
||||
private static final String ACCESS_KEY_SECRET = "XLhp5w4WnRcqji204GYOm7hZsoXSB6";
|
||||
private static final String BUCKET_NAME = "ct-upimg";
|
||||
private static final String URL = "https://ct-upimg.yx090.com";
|
||||
private static final String PREFIX = "ju8hn6/shop";
|
||||
|
||||
public OSS getClient() {
|
||||
return new OSSClientBuilder().build(ENDPOINT, ACCESS_KEY_ID, ACCESS_KEY_SECRET);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到 OSS
|
||||
*
|
||||
* @param localFile 本地临时文件
|
||||
* @return 可访问的 URL
|
||||
*/
|
||||
public String upload(File localFile) {
|
||||
String objectName = generateOssObjectName(localFile.getPath());
|
||||
log.info("文件名称{}", objectName);
|
||||
return upload(localFile, objectName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到 OSS
|
||||
*
|
||||
* @param localFile 本地临时文件
|
||||
* @param objectName OSS 上的文件路径,如 media/2025/09/photo.jpg
|
||||
* @return 可访问的 URL
|
||||
*/
|
||||
public String upload(File localFile, String objectName) {
|
||||
OSS ossClient = null;
|
||||
try {
|
||||
ossClient = getClient();
|
||||
|
||||
// 设置元数据(主要是 Content-Type)
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
String contentType = getContentType(objectName);
|
||||
metadata.setContentType(contentType);
|
||||
|
||||
// 执行上传
|
||||
ossClient.putObject(BUCKET_NAME, objectName, localFile, metadata);
|
||||
OSSObject ossObject = ossClient.getObject(BUCKET_NAME, objectName);
|
||||
System.out.println("文件存在,大小为: " + ossObject.getObjectMetadata().getContentLength());
|
||||
// 生成可访问的 URL
|
||||
// 格式:https://bucket.oss-cn-hangzhou.aliyuncs.com/objectName
|
||||
String url = String.format("%s/%s", URL, objectName);
|
||||
|
||||
return url;
|
||||
} finally {
|
||||
if (ossClient != null) {
|
||||
ossClient.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 OSS 文件路径:年/月/日/40位随机字符串.扩展名
|
||||
*
|
||||
* @param originalFileName 原始文件名,如 avatar.jpg
|
||||
* @return 路径字符串,如 2025/09/25/a1b2c3d4e5f67890123456789012345678901234.jpg
|
||||
*/
|
||||
public static String generateOssObjectName(String originalFileName) {
|
||||
// 1. 获取当前日期:年/月/日
|
||||
LocalDate now = LocalDate.now();
|
||||
String datePath = now.format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
||||
|
||||
// 2. 提取文件扩展名
|
||||
String extension = "";
|
||||
int lastDotIndex = originalFileName.lastIndexOf('.');
|
||||
if (lastDotIndex > 0) {
|
||||
extension = originalFileName.substring(lastDotIndex); // 包含 .
|
||||
} else {
|
||||
extension = ".bin"; // 无扩展名时默认
|
||||
}
|
||||
|
||||
// 3. 生成 40 位随机字符串(a-z,0-9)
|
||||
String randomString = generateRandomString(40);
|
||||
|
||||
// 4. 拼接路径
|
||||
return PREFIX + "/" + datePath + "/" + randomString + extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定长度的随机字符串(十六进制风格)
|
||||
*/
|
||||
private static String generateRandomString(int length) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (sb.length() < length) {
|
||||
sb.append(java.util.UUID.randomUUID().toString().replace("-", ""));
|
||||
}
|
||||
return sb.substring(0, length);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据文件名后缀推断 Content-Type
|
||||
*/
|
||||
private String getContentType(String fileName) {
|
||||
if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
|
||||
return "image/jpeg";
|
||||
} else if (fileName.endsWith(".png")) {
|
||||
return "image/png";
|
||||
} else if (fileName.endsWith(".gif")) {
|
||||
return "image/gif";
|
||||
} else if (fileName.endsWith(".webp")) {
|
||||
return "image/webp";
|
||||
} else if (fileName.endsWith(".mp4")) {
|
||||
return "video/mp4";
|
||||
} else if (fileName.endsWith(".avi")) {
|
||||
return "video/avi";
|
||||
} else if (fileName.endsWith(".pdf")) {
|
||||
return "application/pdf";
|
||||
} else if (fileName.endsWith(".doc")) {
|
||||
return "application/msword";
|
||||
} else if (fileName.endsWith(".docx")) {
|
||||
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||
} else if (fileName.endsWith(".xlsx")) {
|
||||
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
} else {
|
||||
return "application/octet-stream"; // 默认二进制流
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -15,3 +15,7 @@ mybatis-plus:
|
||||
id-type: auto
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
wechat:
|
||||
media:
|
||||
temp-dir: ./temp
|
||||
Loading…
x
Reference in New Issue
Block a user