software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -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;
/**
* 资源管理业务服务
*
* 负责资源的完整生命周期管理:
* - 资源元数据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);
}
}