Files
2026-03-22 15:24:40 +08:00

383 lines
14 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 自然写教学资源管理与内容分发系统软件 V1.0
* service/ResourceService.java - 资源管理业务服务
*/
package com.writech.resource.service;
import java.util.*;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
* 资源管理业务服务
*
* 负责资源的完整生命周期管理:
* - 资源元数据CRUDMySQL
* - 文件存储管理(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);
}
}