This commit is contained in:
杨建炊 2025-09-26 15:00:41 +08:00
parent acb8a77c53
commit cba4790374
21 changed files with 703 additions and 50 deletions

2
.idea/misc.xml generated
View File

@ -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>

View File

@ -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>

View File

@ -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();

View File

@ -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方法解密密文数据

View File

@ -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;
}
}*/
}

View 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;
}

View 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
}

View 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;
}

View 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;
}

View File

@ -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;

View File

@ -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);
}

View 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> {
}

View 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> {
}

View 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> {
}

View 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;
}

View 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> {
}

View File

@ -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;
}

View 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);
}
}

View 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;
}
}

View 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"; // 默认二进制流
}
}
}

View File

@ -15,3 +15,7 @@ mybatis-plus:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
wechat:
media:
temp-dir: ./temp