software copyright
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* 自然写教学资源管理与内容分发系统软件 V1.0
|
||||
* service/ResourceService.java - 资源管理业务服务
|
||||
*/
|
||||
package com.writech.resource.service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 资源管理业务服务
|
||||
*
|
||||
* 负责资源的完整生命周期管理:
|
||||
* - 资源元数据CRUD(MySQL)
|
||||
* - 文件存储管理(OSS/MinIO对象存储)
|
||||
* - 全文索引管理(Elasticsearch)
|
||||
* - CDN缓存管理
|
||||
* - 版本控制
|
||||
* - 数字水印
|
||||
*/
|
||||
public class ResourceService {
|
||||
|
||||
private static final Logger logger =
|
||||
Logger.getLogger(ResourceService.class.getName());
|
||||
|
||||
// ============================================================
|
||||
// 配置常量
|
||||
// ============================================================
|
||||
|
||||
/** 支持的文件类型及最大大小(MB) */
|
||||
private static final Map<String, Integer> ALLOWED_FILE_TYPES = new HashMap<>();
|
||||
static {
|
||||
ALLOWED_FILE_TYPES.put("application/pdf", 100);
|
||||
ALLOWED_FILE_TYPES.put("application/vnd.ms-powerpoint", 200);
|
||||
ALLOWED_FILE_TYPES.put("application/vnd.openxmlformats-officedocument.presentationml.presentation", 200);
|
||||
ALLOWED_FILE_TYPES.put("image/jpeg", 20);
|
||||
ALLOWED_FILE_TYPES.put("image/png", 20);
|
||||
ALLOWED_FILE_TYPES.put("image/svg+xml", 10);
|
||||
ALLOWED_FILE_TYPES.put("video/mp4", 500);
|
||||
ALLOWED_FILE_TYPES.put("audio/mpeg", 50);
|
||||
}
|
||||
|
||||
/** OSS存储桶名称 */
|
||||
private static final String OSS_BUCKET = "writech-resources";
|
||||
|
||||
/** 缩略图存储前缀 */
|
||||
private static final String THUMBNAIL_PREFIX = "thumbnails/";
|
||||
|
||||
/** Elasticsearch索引名称 */
|
||||
private static final String ES_INDEX = "writech_resources";
|
||||
|
||||
// ============================================================
|
||||
// 数据模型
|
||||
// ============================================================
|
||||
|
||||
/** 资源版本记录 */
|
||||
public static class ResourceVersion {
|
||||
private String id;
|
||||
private String resourceId;
|
||||
private int versionNumber;
|
||||
private String fileKey;
|
||||
private long fileSize;
|
||||
private String changeLog;
|
||||
private String operatorId;
|
||||
private Date createdAt;
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
public String getResourceId() { return resourceId; }
|
||||
public void setResourceId(String rid) { this.resourceId = rid; }
|
||||
public int getVersionNumber() { return versionNumber; }
|
||||
public void setVersionNumber(int v) { this.versionNumber = v; }
|
||||
public String getFileKey() { return fileKey; }
|
||||
public void setFileKey(String key) { this.fileKey = key; }
|
||||
public String getChangeLog() { return changeLog; }
|
||||
public void setChangeLog(String log) { this.changeLog = log; }
|
||||
public Date getCreatedAt() { return createdAt; }
|
||||
}
|
||||
|
||||
/** 数字水印配置 */
|
||||
public static class WatermarkConfig {
|
||||
private String text; // 水印文字(学校名+教师名)
|
||||
private float opacity; // 透明度(0.0-1.0)
|
||||
private int fontSize; // 字号
|
||||
private float rotation; // 旋转角度
|
||||
private String position; // 位置:center/bottom-right/tiled
|
||||
|
||||
public WatermarkConfig(String text) {
|
||||
this.text = text;
|
||||
this.opacity = 0.15f;
|
||||
this.fontSize = 24;
|
||||
this.rotation = -30.0f;
|
||||
this.position = "tiled";
|
||||
}
|
||||
|
||||
public String getText() { return text; }
|
||||
public float getOpacity() { return opacity; }
|
||||
public int getFontSize() { return fontSize; }
|
||||
public float getRotation() { return rotation; }
|
||||
public String getPosition() { return position; }
|
||||
}
|
||||
|
||||
/** STS临时上传凭证 */
|
||||
public static class UploadCredential {
|
||||
private String accessKeyId;
|
||||
private String accessKeySecret;
|
||||
private String securityToken;
|
||||
private String bucket;
|
||||
private String objectKeyPrefix;
|
||||
private long expireTimeSeconds;
|
||||
|
||||
public String getAccessKeyId() { return accessKeyId; }
|
||||
public String getAccessKeySecret() { return accessKeySecret; }
|
||||
public String getSecurityToken() { return securityToken; }
|
||||
public String getBucket() { return bucket; }
|
||||
public String getObjectKeyPrefix() { return objectKeyPrefix; }
|
||||
public long getExpireTimeSeconds() { return expireTimeSeconds; }
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 业务方法
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 获取STS临时上传凭证
|
||||
*
|
||||
* 前端使用STS凭证直接上传到OSS,避免文件经过应用服务器。
|
||||
* STS凭证限制:仅允许PUT到指定前缀路径,有效期15分钟。
|
||||
*
|
||||
* @param uploaderId 上传者ID
|
||||
* @param fileType 文件MIME类型
|
||||
* @return STS临时凭证
|
||||
*/
|
||||
public UploadCredential getUploadCredential(String uploaderId, String fileType) {
|
||||
logger.info(String.format("获取上传凭证: user=%s, type=%s", uploaderId, fileType));
|
||||
|
||||
// 校验文件类型
|
||||
if (!ALLOWED_FILE_TYPES.containsKey(fileType)) {
|
||||
throw new IllegalArgumentException("不支持的文件类型: " + fileType);
|
||||
}
|
||||
|
||||
// 生成上传路径前缀:resources/{uploaderId}/{year}/{month}/
|
||||
Calendar cal = Calendar.getInstance();
|
||||
String prefix = String.format(
|
||||
"resources/%s/%d/%02d/",
|
||||
uploaderId,
|
||||
cal.get(Calendar.YEAR),
|
||||
cal.get(Calendar.MONTH) + 1
|
||||
);
|
||||
|
||||
// 调用OSS STS服务获取临时凭证
|
||||
// AssumeRoleResponse response = stsClient.assumeRole(
|
||||
// roleArn, policy, sessionName, 900 // 15分钟
|
||||
// );
|
||||
|
||||
UploadCredential credential = new UploadCredential();
|
||||
// credential.accessKeyId = response.getCredentials().getAccessKeyId();
|
||||
// credential.accessKeySecret = response.getCredentials().getAccessKeySecret();
|
||||
// credential.securityToken = response.getCredentials().getSecurityToken();
|
||||
// credential.bucket = OSS_BUCKET;
|
||||
// credential.objectKeyPrefix = prefix;
|
||||
// credential.expireTimeSeconds = 900;
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建资源记录(上传完成后调用)
|
||||
*
|
||||
* @param metadata 资源元数据
|
||||
* @return 创建的资源ID
|
||||
*/
|
||||
public String createResource(Map<String, Object> metadata) {
|
||||
String name = (String) metadata.get("name");
|
||||
String fileKey = (String) metadata.get("file_key");
|
||||
String mimeType = (String) metadata.get("mime_type");
|
||||
|
||||
logger.info(String.format("创建资源: name=%s, key=%s", name, fileKey));
|
||||
|
||||
// 生成资源ID
|
||||
String resourceId = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
// 自动生成缩略图
|
||||
generateThumbnailAsync(resourceId, fileKey, mimeType);
|
||||
|
||||
// 插入MySQL元数据
|
||||
// resourceMapper.insert(resource);
|
||||
|
||||
// 创建初始版本记录
|
||||
createVersion(resourceId, fileKey, "初始版本", (String) metadata.get("uploader_id"));
|
||||
|
||||
// 同步索引到Elasticsearch
|
||||
indexToElasticsearch(resourceId, metadata);
|
||||
|
||||
logger.info("资源创建成功: id=" + resourceId);
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新资源(新版本上传)
|
||||
*
|
||||
* 资源更新不删除旧版本,而是创建新版本记录,
|
||||
* 支持版本回滚。更新后需刷新CDN缓存。
|
||||
*/
|
||||
public void updateResource(String resourceId, Map<String, Object> updateData) {
|
||||
logger.info("更新资源: id=" + resourceId);
|
||||
|
||||
String newFileKey = (String) updateData.get("file_key");
|
||||
String changeLog = (String) updateData.get("change_log");
|
||||
String operatorId = (String) updateData.get("operator_id");
|
||||
|
||||
// 创建新版本
|
||||
if (newFileKey != null) {
|
||||
createVersion(resourceId, newFileKey, changeLog, operatorId);
|
||||
}
|
||||
|
||||
// 更新MySQL元数据
|
||||
// resourceMapper.update(resourceId, updateData);
|
||||
|
||||
// 更新Elasticsearch索引
|
||||
updateElasticsearchIndex(resourceId, updateData);
|
||||
|
||||
// 刷新CDN缓存
|
||||
refreshCdnCache(resourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建版本记录
|
||||
*/
|
||||
private void createVersion(
|
||||
String resourceId, String fileKey, String changeLog, String operatorId
|
||||
) {
|
||||
// 查询当前最大版本号
|
||||
// int maxVersion = versionMapper.selectMaxVersion(resourceId);
|
||||
|
||||
ResourceVersion version = new ResourceVersion();
|
||||
version.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
version.setResourceId(resourceId);
|
||||
version.setVersionNumber(1); // maxVersion + 1
|
||||
version.setFileKey(fileKey);
|
||||
version.setChangeLog(changeLog);
|
||||
|
||||
// versionMapper.insert(version);
|
||||
logger.info(String.format(
|
||||
"创建版本: resource=%s, version=%d", resourceId, version.getVersionNumber()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步生成缩略图
|
||||
*
|
||||
* 根据文件类型采用不同策略:
|
||||
* - PDF: 渲染第一页为图片
|
||||
* - PPT: 提取封面幻灯片
|
||||
* - 图片: 直接缩放
|
||||
* - 视频: 提取关键帧
|
||||
*/
|
||||
private void generateThumbnailAsync(String resourceId, String fileKey, String mimeType) {
|
||||
// @Async 异步执行
|
||||
logger.info(String.format(
|
||||
"生成缩略图: resource=%s, type=%s", resourceId, mimeType
|
||||
));
|
||||
|
||||
// 根据MIME类型选择缩略图生成策略
|
||||
// if (mimeType.equals("application/pdf")) {
|
||||
// PDDocument doc = PDDocument.load(ossClient.getObject(fileKey));
|
||||
// PDFRenderer renderer = new PDFRenderer(doc);
|
||||
// BufferedImage image = renderer.renderImageWithDPI(0, 150);
|
||||
// // 缩放为缩略图尺寸(320x240)
|
||||
// BufferedImage thumb = ImageUtils.resize(image, 320, 240);
|
||||
// // 上传缩略图到OSS
|
||||
// ossClient.putObject(THUMBNAIL_PREFIX + resourceId + ".jpg", thumb);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 索引资源到Elasticsearch
|
||||
*
|
||||
* 索引字段:名称、描述、标签、学科、年级、出版社、类型
|
||||
* 支持中文分词(IK分词器)
|
||||
*/
|
||||
private void indexToElasticsearch(String resourceId, Map<String, Object> metadata) {
|
||||
logger.info("索引资源到ES: id=" + resourceId);
|
||||
|
||||
// Map<String, Object> document = new HashMap<>();
|
||||
// document.put("id", resourceId);
|
||||
// document.put("name", metadata.get("name"));
|
||||
// document.put("description", metadata.get("description"));
|
||||
// document.put("tags", metadata.get("tags"));
|
||||
// document.put("subject", metadata.get("subject"));
|
||||
// document.put("grade", metadata.get("grade"));
|
||||
// document.put("publisher", metadata.get("publisher"));
|
||||
// document.put("type", metadata.get("type"));
|
||||
// document.put("school_id", metadata.get("school_id"));
|
||||
// document.put("audit_status", "PENDING");
|
||||
// document.put("created_at", new Date());
|
||||
|
||||
// IndexRequest request = new IndexRequest(ES_INDEX)
|
||||
// .id(resourceId)
|
||||
// .source(document);
|
||||
// elasticsearchClient.index(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Elasticsearch索引
|
||||
*/
|
||||
private void updateElasticsearchIndex(String resourceId, Map<String, Object> updateData) {
|
||||
// UpdateRequest request = new UpdateRequest(ES_INDEX, resourceId)
|
||||
// .doc(updateData);
|
||||
// elasticsearchClient.update(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新CDN缓存
|
||||
*
|
||||
* 资源更新后需要刷新CDN节点缓存,确保终端获取最新版本。
|
||||
*/
|
||||
private void refreshCdnCache(String resourceId) {
|
||||
logger.info("刷新CDN缓存: resource=" + resourceId);
|
||||
// String cdnUrl = String.format("https://cdn.writech.com/resources/%s", resourceId);
|
||||
// cdnClient.refreshObjectCaches(Collections.singletonList(cdnUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加数字水印
|
||||
*
|
||||
* 下载资源时可选添加数字水印,水印包含学校和教师标识,
|
||||
* 用于版权保护和追踪。
|
||||
*
|
||||
* @param fileBytes 原始文件字节
|
||||
* @param config 水印配置
|
||||
* @return 添加水印后的文件字节
|
||||
*/
|
||||
public byte[] addWatermark(byte[] fileBytes, WatermarkConfig config) {
|
||||
logger.info("添加数字水印: text=" + config.getText());
|
||||
|
||||
// PDF水印添加
|
||||
// PDDocument doc = PDDocument.load(fileBytes);
|
||||
// for (PDPage page : doc.getPages()) {
|
||||
// PDPageContentStream cs = new PDPageContentStream(doc, page, APPEND, true);
|
||||
// cs.setFont(PDType1Font.HELVETICA, config.getFontSize());
|
||||
// cs.setNonStrokingColor(200, 200, 200); // 浅灰色
|
||||
// // 平铺水印
|
||||
// for (float y = 0; y < page.getMediaBox().getHeight(); y += 100) {
|
||||
// for (float x = 0; x < page.getMediaBox().getWidth(); x += 200) {
|
||||
// cs.beginText();
|
||||
// Matrix matrix = Matrix.getRotateInstance(
|
||||
// Math.toRadians(config.getRotation()), x, y
|
||||
// );
|
||||
// cs.setTextMatrix(matrix);
|
||||
// cs.showText(config.getText());
|
||||
// cs.endText();
|
||||
// }
|
||||
// }
|
||||
// cs.close();
|
||||
// }
|
||||
|
||||
return fileBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除资源(软删除)
|
||||
*
|
||||
* 不物理删除文件,仅标记为已删除状态。
|
||||
* OSS文件通过生命周期策略定期清理。
|
||||
*/
|
||||
public void deleteResource(String resourceId, String operatorId) {
|
||||
logger.info(String.format(
|
||||
"删除资源: id=%s, operator=%s", resourceId, operatorId
|
||||
));
|
||||
|
||||
// 软删除:更新状态
|
||||
// resourceMapper.updateStatus(resourceId, "DELETED");
|
||||
|
||||
// 从ES索引中移除
|
||||
// elasticsearchClient.delete(new DeleteRequest(ES_INDEX, resourceId));
|
||||
|
||||
// 刷新CDN
|
||||
refreshCdnCache(resourceId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user