更改
This commit is contained in:
parent
acb8a77c53
commit
cba4790374
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -8,7 +8,7 @@
|
|||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</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" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
6
pom.xml
6
pom.xml
@ -55,6 +55,12 @@
|
|||||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||||
<version>3.5.6</version> <!-- 推荐使用最新稳定版 -->
|
<version>3.5.6</version> <!-- 推荐使用最新稳定版 -->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.aliyun.oss</groupId>
|
||||||
|
<artifactId>aliyun-sdk-oss</artifactId>
|
||||||
|
<version>3.17.4</version> <!-- 推荐使用最新版 -->
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</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 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();
|
public native static long NewSlice();
|
||||||
|
|
||||||
|
|||||||
@ -1,45 +1,66 @@
|
|||||||
package org.shop.crop;
|
package org.shop.crop;
|
||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.tencent.wework.Finance;
|
import com.tencent.wework.Finance;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.BaseMessageDto;
|
||||||
import org.shop.crop.dto.ChatData;
|
import org.shop.crop.dto.ChatData;
|
||||||
import org.shop.crop.dto.Message;
|
|
||||||
import org.shop.crop.dto.MessagePullResponse;
|
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.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.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.util.StreamUtils;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
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.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.List;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
public class CropController {
|
public class CropController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
Long sdk;
|
FinanceSdkManager sdkManager;
|
||||||
|
|
||||||
@Autowired
|
@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")
|
@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 limit = 1000;
|
||||||
long slice = Finance.NewSlice();
|
long slice = Finance.NewSlice();
|
||||||
|
Long sdk = sdkManager.initSdk(corpId, "mEizahrSF6axdfWtSK_f73a3j6-sV02hhyGG7ogmTpM");
|
||||||
|
|
||||||
int ret = Finance.GetChatData(sdk, startSeq, limit, "", "", 1000, slice);
|
int ret = Finance.GetChatData(sdk, startSeq, limit, "", "", 1000, slice);
|
||||||
if (ret != 0) {
|
if (ret != 0) {
|
||||||
System.out.println("调用sdk拉取消息接口失败,失败消息为 ret = " + ret);
|
System.out.println("调用sdk拉取消息接口失败,失败消息为 ret = " + ret);
|
||||||
@ -48,7 +69,7 @@ public class CropController {
|
|||||||
}
|
}
|
||||||
String contentResult = Finance.GetContentFromSlice(slice);
|
String contentResult = Finance.GetContentFromSlice(slice);
|
||||||
log.info("内容字符串" + contentResult);
|
log.info("内容字符串" + contentResult);
|
||||||
return decodeChatData(contentResult);
|
return decodeChatData(contentResult,startSeq,sdk,corpId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,9 +77,9 @@ public class CropController {
|
|||||||
*
|
*
|
||||||
* @param contentResult 拉取到的JSON原文
|
* @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.基础信息解析
|
// 1.基础信息解析
|
||||||
List<Message> messageList = new ArrayList<>();
|
List<CropMessage> messageList = new ArrayList<>();
|
||||||
MessagePullResponse messagePullResponse = JSON.parseObject(contentResult, MessagePullResponse.class);
|
MessagePullResponse messagePullResponse = JSON.parseObject(contentResult, MessagePullResponse.class);
|
||||||
if (messagePullResponse == null || messagePullResponse.getChatdata() == null) {
|
if (messagePullResponse == null || messagePullResponse.getChatdata() == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
@ -70,27 +91,203 @@ public class CropController {
|
|||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
String privateKey = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8);
|
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) {
|
for (ChatData chatData : chatdataList) {
|
||||||
|
// ✅ 如果 seq 已存在,跳过
|
||||||
|
if (existingSeqSet.contains(chatData.getSeq())) {
|
||||||
|
log.debug("seq={} 已存在于数据库,跳过解密和插入", chatData.getSeq());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
// 解密ChatData数据
|
// 解密ChatData数据
|
||||||
String plainTextJson = decrypt(chatData, privateKey);
|
String plainTextJson = decrypt(chatData, privateKey,sdk);
|
||||||
BaseMessageDto messageDto = JSON.parseObject(plainTextJson, BaseMessageDto.class);
|
BaseMessageDto messageDto = JSON.parseObject(plainTextJson, BaseMessageDto.class);
|
||||||
// 转换dto数据到entity
|
// 转换dto数据到entity
|
||||||
Message message = messageDto.convertToMessage();
|
CropMessage message = messageDto.convertToMessage();
|
||||||
|
message.setCropId(corpId);
|
||||||
message.setSeq(chatData.getSeq());
|
message.setSeq(chatData.getSeq());
|
||||||
message.setOriginContent(plainTextJson);
|
message.setContent(plainTextJson);
|
||||||
messageList.add(message);
|
messageList.add(message);
|
||||||
|
// ✅ 提取 sdkfileid
|
||||||
|
List<CropFile> files = extractMediaFiles(plainTextJson, chatData.getSeq());
|
||||||
|
mediaFileInfos.addAll(files); // 收集所有待下载文件
|
||||||
|
|
||||||
|
if (chatData.getSeq() > maxSeq) {
|
||||||
|
maxSeq = chatData.getSeq();
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("解密失败{}", e.getMessage());
|
log.error("解密失败{}", e.getMessage());
|
||||||
// 收集失败情况数据
|
// 收集失败情况数据
|
||||||
e.printStackTrace();
|
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.保存消息和失败任务
|
// 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;
|
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 私钥字符串
|
* @param privateKeyStr 私钥字符串
|
||||||
* @return 解密后的原文
|
* @return 解密后的原文
|
||||||
*/
|
*/
|
||||||
private String decrypt(ChatData chatData, String privateKeyStr) throws Exception {
|
private String decrypt(ChatData chatData, String privateKeyStr,Long sdk) throws Exception {
|
||||||
// 1.解密EncryptRandomKey
|
// 1.解密EncryptRandomKey
|
||||||
String randomKey = EncodeUtils.decryptRandomKey(chatData.getEncryptRandomKey(), privateKeyStr);
|
String randomKey = EncodeUtils.decryptRandomKey(chatData.getEncryptRandomKey(), privateKeyStr);
|
||||||
// 2.调用SDK方法解密密文数据
|
// 2.调用SDK方法解密密文数据
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class MyConfig {
|
public class MyConfig {
|
||||||
|
|
||||||
@Bean
|
/* @Bean
|
||||||
public long sdk() {
|
public long sdk() {
|
||||||
System.out.println("Initializing SDK...");
|
System.out.println("Initializing SDK...");
|
||||||
long sdk = Finance.NewSdk();
|
long sdk = Finance.NewSdk();
|
||||||
@ -22,5 +22,5 @@ public class MyConfig {
|
|||||||
throw new RuntimeException("初始化sdk失败,失败消息为:ret = " + init);
|
throw new RuntimeException("初始化sdk失败,失败消息为:ret = " + init);
|
||||||
}
|
}
|
||||||
return sdk;
|
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;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("crop_message")
|
@TableName("crop_messages")
|
||||||
public class CropMessage {
|
public class CropMessage {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主键 ID(自增)
|
* 主键 ID(自增)
|
||||||
*/
|
*/
|
||||||
@ -28,6 +27,12 @@ public class CropMessage {
|
|||||||
@TableField("msg_id")
|
@TableField("msg_id")
|
||||||
private String msgId;
|
private String msgId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@TableField("cropid")
|
||||||
|
private String cropId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息动作(如 send, reply, delete)
|
* 消息动作(如 send, reply, delete)
|
||||||
*/
|
*/
|
||||||
@ -37,7 +42,7 @@ public class CropMessage {
|
|||||||
/**
|
/**
|
||||||
* 发送方 ID
|
* 发送方 ID
|
||||||
*/
|
*/
|
||||||
@TableField("from")
|
@TableField("from_id")
|
||||||
private String from; // 注意:'from' 是关键字,不能直接写,所以用注解指定
|
private String from; // 注意:'from' 是关键字,不能直接写,所以用注解指定
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,6 +51,12 @@ public class CropMessage {
|
|||||||
@TableField("tolist")
|
@TableField("tolist")
|
||||||
private String tolist;
|
private String tolist;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收方列表(JSON 或逗号分隔)
|
||||||
|
*/
|
||||||
|
@TableField("to_id")
|
||||||
|
private String toId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 群聊消息的房间 ID
|
* 群聊消息的房间 ID
|
||||||
*/
|
*/
|
||||||
@ -74,7 +85,7 @@ public class CropMessage {
|
|||||||
* 消息发送时间
|
* 消息发送时间
|
||||||
*/
|
*/
|
||||||
@TableField("msgtime")
|
@TableField("msgtime")
|
||||||
private LocalDateTime msgtime;
|
private String msgtime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息状态(是否已处理)
|
* 消息状态(是否已处理)
|
||||||
@ -82,12 +93,6 @@ public class CropMessage {
|
|||||||
@TableField("status")
|
@TableField("status")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建时间(自动填充)
|
|
||||||
*/
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
|
||||||
private LocalDateTime createdTime;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 逻辑删除标志(0:正常,1:已删除)
|
* 逻辑删除标志(0:正常,1:已删除)
|
||||||
*/
|
*/
|
||||||
@ -98,14 +103,14 @@ public class CropMessage {
|
|||||||
/**
|
/**
|
||||||
* 创建时间(备用字段,可选)
|
* 创建时间(备用字段,可选)
|
||||||
*/
|
*/
|
||||||
@TableField("gmt_created")
|
@TableField("created_at")
|
||||||
private LocalDateTime gmtCreated;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改时间(自动填充)
|
* 修改时间(自动填充)
|
||||||
*/
|
*/
|
||||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
@TableField("updated_at")
|
||||||
private LocalDateTime gmtModified;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSON;
|
|||||||
import com.alibaba.fastjson.annotation.JSONField;
|
import com.alibaba.fastjson.annotation.JSONField;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.shop.crop.dao.CropMessage;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@ -33,7 +34,7 @@ public class BaseMessageDto {
|
|||||||
* 消息发送方id
|
* 消息发送方id
|
||||||
*/
|
*/
|
||||||
@JSONField(name = "from")
|
@JSONField(name = "from")
|
||||||
private String fromId;
|
private String from;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消息接收方列表
|
* 消息接收方列表
|
||||||
@ -62,25 +63,26 @@ public class BaseMessageDto {
|
|||||||
|
|
||||||
// getter setter
|
// getter setter
|
||||||
|
|
||||||
public Message convertToMessage(){
|
public CropMessage convertToMessage(){
|
||||||
Message message = new Message();
|
CropMessage message = new CropMessage();
|
||||||
message.setMsgId(this.msgId);
|
message.setMsgId(this.msgId);
|
||||||
message.setAction(this.action);
|
message.setAction(this.action);
|
||||||
message.setFromId(this.fromId);
|
message.setFrom(this.from);
|
||||||
message.setMsgTime(this.msgTime);
|
message.setMsgtype(this.msgType);
|
||||||
message.setMsgType(this.msgType);
|
message.setRoomid(this.roomId);
|
||||||
message.setRoomId(this.roomId);
|
|
||||||
// id数组转为字符串
|
// id数组转为字符串
|
||||||
message.setToList(JSON.toJSONString(this.toList));
|
message.setTolist(JSON.toJSONString(this.toList));
|
||||||
|
message.setToId(this.toList.get(0));
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
if(this.getMsgTime() != null){
|
if(this.getMsgTime() != null){
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");;
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");;
|
||||||
|
long seconds = this.getMsgTime() / 1000;
|
||||||
LocalDateTime dateTime = LocalDateTime.ofInstant(
|
LocalDateTime dateTime = LocalDateTime.ofInstant(
|
||||||
Instant.ofEpochSecond(this.getMsgTime()),
|
Instant.ofEpochSecond(seconds),
|
||||||
ZoneId.systemDefault()
|
ZoneId.systemDefault()
|
||||||
);
|
);
|
||||||
String timeStr=dateTime.format(formatter);
|
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"; // 默认二进制流
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,4 +14,8 @@ mybatis-plus:
|
|||||||
db-config:
|
db-config:
|
||||||
id-type: auto
|
id-type: auto
|
||||||
logic-delete-value: 1
|
logic-delete-value: 1
|
||||||
logic-not-delete-value: 0
|
logic-not-delete-value: 0
|
||||||
|
|
||||||
|
wechat:
|
||||||
|
media:
|
||||||
|
temp-dir: ./temp
|
||||||
Loading…
x
Reference in New Issue
Block a user