diff --git a/.idea/misc.xml b/.idea/misc.xml index 9e1b5e9..56cccdf 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/pom.xml b/pom.xml index bd6121b..abc624f 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,12 @@ mybatis-plus-boot-starter 3.5.6 + + com.aliyun.oss + aliyun-sdk-oss + 3.17.4 + + diff --git a/src/main/java/com/tencent/wework/Finance.java b/src/main/java/com/tencent/wework/Finance.java index e72e221..96c3aa1 100644 --- a/src/main/java/com/tencent/wework/Finance.java +++ b/src/main/java/com/tencent/wework/Finance.java @@ -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(); diff --git a/src/main/java/org/shop/crop/CropController.java b/src/main/java/org/shop/crop/CropController.java index d05dc14..730a24e 100644 --- a/src/main/java/org/shop/crop/CropController.java +++ b/src/main/java/org/shop/crop/CropController.java @@ -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 chatData(Long startSeq) throws IOException { + public List 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 decodeChatData(String contentResult) throws IOException { + public List decodeChatData(String contentResult,Long startSeq,Long sdk,String corpId) throws IOException { // 1.基础信息解析 - List messageList = new ArrayList<>(); + List 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 existingSeqs = cropMessageService.lambdaQuery() + .gt(CropMessage::getSeq, startSeq) + .select(CropMessage::getSeq) + .list() + .stream() + .map(CropMessage::getSeq) + .collect(Collectors.toList()); + Set existingSeqSet = new HashSet<>(existingSeqs); + log.info("existingSeqSet={} 加密数据",existingSeqSet); + + CropConfig config=cropConfigMapper.selectById(1); + Long maxSeq = config.getLastSeq(); + // 改为: + List 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 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 + */ + @Transactional + public List saveData(List messageList, Long maxSeq, CropConfig config + , ListmediaFileInfos,Long sdk,String corpId) { + // 4.保存消息和失败任务 + cropMessageService.saveBatch(messageList); + config.setLastSeq(maxSeq); + cropConfigMapper.updateById(config); + + if (!mediaFileInfos.isEmpty()) { + List 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 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 extractMediaFiles(String plainTextJson, Long seq) { + List 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方法解密密文数据 diff --git a/src/main/java/org/shop/crop/MyConfig.java b/src/main/java/org/shop/crop/MyConfig.java index 9f32ea5..c5f66a4 100644 --- a/src/main/java/org/shop/crop/MyConfig.java +++ b/src/main/java/org/shop/crop/MyConfig.java @@ -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; - } + }*/ } diff --git a/src/main/java/org/shop/crop/config/MediaProperties.java b/src/main/java/org/shop/crop/config/MediaProperties.java new file mode 100644 index 0000000..03b3451 --- /dev/null +++ b/src/main/java/org/shop/crop/config/MediaProperties.java @@ -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; +} diff --git a/src/main/java/org/shop/crop/constant/GlobalConstants.java b/src/main/java/org/shop/crop/constant/GlobalConstants.java new file mode 100644 index 0000000..671a873 --- /dev/null +++ b/src/main/java/org/shop/crop/constant/GlobalConstants.java @@ -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 等 +} diff --git a/src/main/java/org/shop/crop/dao/CropConfig.java b/src/main/java/org/shop/crop/dao/CropConfig.java new file mode 100644 index 0000000..e0378ee --- /dev/null +++ b/src/main/java/org/shop/crop/dao/CropConfig.java @@ -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; + + +} diff --git a/src/main/java/org/shop/crop/dao/CropFile.java b/src/main/java/org/shop/crop/dao/CropFile.java new file mode 100644 index 0000000..0601b14 --- /dev/null +++ b/src/main/java/org/shop/crop/dao/CropFile.java @@ -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; +} diff --git a/src/main/java/org/shop/crop/dao/CropMessage.java b/src/main/java/org/shop/crop/dao/CropMessage.java index 1f3a955..e101c1c 100644 --- a/src/main/java/org/shop/crop/dao/CropMessage.java +++ b/src/main/java/org/shop/crop/dao/CropMessage.java @@ -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; diff --git a/src/main/java/org/shop/crop/dto/BaseMessageDto.java b/src/main/java/org/shop/crop/dto/BaseMessageDto.java index 292922d..d176079 100644 --- a/src/main/java/org/shop/crop/dto/BaseMessageDto.java +++ b/src/main/java/org/shop/crop/dto/BaseMessageDto.java @@ -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); } diff --git a/src/main/java/org/shop/crop/mapper/CropConfigMapper.java b/src/main/java/org/shop/crop/mapper/CropConfigMapper.java new file mode 100644 index 0000000..d6f6a57 --- /dev/null +++ b/src/main/java/org/shop/crop/mapper/CropConfigMapper.java @@ -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 { +} diff --git a/src/main/java/org/shop/crop/mapper/CropFileMapper.java b/src/main/java/org/shop/crop/mapper/CropFileMapper.java new file mode 100644 index 0000000..4fac0a5 --- /dev/null +++ b/src/main/java/org/shop/crop/mapper/CropFileMapper.java @@ -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 { +} diff --git a/src/main/java/org/shop/crop/service/CropFileService.java b/src/main/java/org/shop/crop/service/CropFileService.java new file mode 100644 index 0000000..652282e --- /dev/null +++ b/src/main/java/org/shop/crop/service/CropFileService.java @@ -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 { + +} diff --git a/src/main/java/org/shop/crop/service/CropFileServiceImpl.java b/src/main/java/org/shop/crop/service/CropFileServiceImpl.java new file mode 100644 index 0000000..383cf38 --- /dev/null +++ b/src/main/java/org/shop/crop/service/CropFileServiceImpl.java @@ -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 + implements CropFileService{ + + @Autowired + private CropFileMapper cropFileMapper; +} diff --git a/src/main/java/org/shop/crop/service/CropMessageService.java b/src/main/java/org/shop/crop/service/CropMessageService.java new file mode 100644 index 0000000..667d177 --- /dev/null +++ b/src/main/java/org/shop/crop/service/CropMessageService.java @@ -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 { + +} diff --git a/src/main/java/org/shop/crop/service/CropMessageServiceImpl.java b/src/main/java/org/shop/crop/service/CropMessageServiceImpl.java new file mode 100644 index 0000000..1cfe8e5 --- /dev/null +++ b/src/main/java/org/shop/crop/service/CropMessageServiceImpl.java @@ -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 + implements CropMessageService{ + + @Autowired + private CropMessageMapper cropMessageMapper; + + +} diff --git a/src/main/java/org/shop/crop/service/FinanceSdkManager.java b/src/main/java/org/shop/crop/service/FinanceSdkManager.java new file mode 100644 index 0000000..e1d56af --- /dev/null +++ b/src/main/java/org/shop/crop/service/FinanceSdkManager.java @@ -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 sdkMap = new ConcurrentHashMap<>(); + + // 存储每个企业的 secret 配置 + private final Map 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); + } + + +} \ No newline at end of file diff --git a/src/main/java/org/shop/crop/service/MediaContext.java b/src/main/java/org/shop/crop/service/MediaContext.java new file mode 100644 index 0000000..82e5b6d --- /dev/null +++ b/src/main/java/org/shop/crop/service/MediaContext.java @@ -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; + } +} + diff --git a/src/main/java/org/shop/crop/service/OssService.java b/src/main/java/org/shop/crop/service/OssService.java new file mode 100644 index 0000000..913ced9 --- /dev/null +++ b/src/main/java/org/shop/crop/service/OssService.java @@ -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"; // 默认二进制流 + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0eb3f73..d801a60 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,4 +14,8 @@ mybatis-plus: db-config: id-type: auto logic-delete-value: 1 - logic-not-delete-value: 0 \ No newline at end of file + logic-not-delete-value: 0 + +wechat: + media: + temp-dir: ./temp \ No newline at end of file