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,342 @@
/*
* 自然写教学资源管理与内容分发系统软件 V1.0
* service/AuditService.java - 内容审核服务
*/
package com.writech.resource.service;
import java.util.*;
import java.util.logging.Logger;
/**
* 内容审核服务
*
* 教师上传的资源需经过管理员审核后才能被其他用户检索和使用。
* 审核流程支持:
* - 自动预审(AI内容安全检测)
* - 人工审核(管理员审核通过/驳回/退回修改)
* - 审核记录全程留痕
* - 批量审核
*/
public class AuditService {
private static final Logger logger =
Logger.getLogger(AuditService.class.getName());
// ============================================================
// 审核数据模型
// ============================================================
/** 审核操作类型 */
public enum AuditAction {
APPROVE, // 审核通过
REJECT, // 驳回
RETURN, // 退回修改
WITHDRAW // 上传者撤回
}
/** 审核记录(对应MySQL audit_record表) */
public static class AuditRecord {
private String id;
private String resourceId;
private String resourceName;
private String auditorId; // 审核人ID
private String auditorName; // 审核人姓名
private AuditAction action;
private String comment; // 审核意见
private String preStatus; // 审核前状态
private String postStatus; // 审核后状态
private Date createdAt;
// Getter/Setter
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 String getResourceName() { return resourceName; }
public void setResourceName(String name) { this.resourceName = name; }
public String getAuditorId() { return auditorId; }
public void setAuditorId(String id) { this.auditorId = id; }
public AuditAction getAction() { return action; }
public void setAction(AuditAction action) { this.action = action; }
public String getComment() { return comment; }
public void setComment(String comment) { this.comment = comment; }
public Date getCreatedAt() { return createdAt; }
}
/** 审核请求 */
public static class AuditRequest {
private String resourceId;
private AuditAction action;
private String comment;
private String auditorId;
public String getResourceId() { return resourceId; }
public void setResourceId(String id) { this.resourceId = id; }
public AuditAction getAction() { return action; }
public void setAction(AuditAction action) { this.action = action; }
public String getComment() { return comment; }
public void setComment(String c) { this.comment = c; }
public String getAuditorId() { return auditorId; }
public void setAuditorId(String id) { this.auditorId = id; }
}
/** 自动预审结果 */
public static class PreAuditResult {
private boolean safe;
private double safeScore; // 安全评分(0-1
private List<String> warnings; // 警告信息
private String category; // 内容分类
public PreAuditResult(boolean safe, double score) {
this.safe = safe;
this.safeScore = score;
this.warnings = new ArrayList<>();
}
public boolean isSafe() { return safe; }
public double getSafeScore() { return safeScore; }
public List<String> getWarnings() { return warnings; }
public void addWarning(String w) { this.warnings.add(w); }
}
// ============================================================
// 审核业务方法
// ============================================================
/**
* 执行审核操作 PUT /api/v1/resource/audit/{id}
*
* @param request 审核请求
* @return 审核结果
*/
public Map<String, Object> performAudit(AuditRequest request) {
logger.info(String.format(
"执行审核: resource=%s, action=%s, auditor=%s",
request.getResourceId(), request.getAction(), request.getAuditorId()
));
// 查询资源当前状态
// ResourceMetadata resource = resourceMapper.selectById(request.getResourceId());
// if (resource == null) {
// return errorResponse(404, "资源不存在");
// }
// 状态机校验:只有PENDING状态可被审核
// if (resource.getAuditStatus() != AuditStatus.PENDING) {
// return errorResponse(400, "当前状态不可审核");
// }
// 创建审核记录
AuditRecord record = new AuditRecord();
record.setId(UUID.randomUUID().toString().replace("-", ""));
record.setResourceId(request.getResourceId());
record.setAuditorId(request.getAuditorId());
record.setAction(request.getAction());
record.setComment(request.getComment());
record.setPreStatus("PENDING");
// 根据审核动作更新资源状态
String newStatus;
switch (request.getAction()) {
case APPROVE:
newStatus = "APPROVED";
// 审核通过后,同步更新Elasticsearch索引状态
// updateEsAuditStatus(request.getResourceId(), "APPROVED");
// 预热CDN缓存(使资源可被终端下载)
// cdnService.preheatResource(request.getResourceId());
break;
case REJECT:
newStatus = "REJECTED";
break;
case RETURN:
newStatus = "PENDING"; // 退回修改后重新提交
break;
default:
newStatus = "PENDING";
}
record.setPostStatus(newStatus);
// 持久化
// auditRecordMapper.insert(record);
// resourceMapper.updateAuditStatus(request.getResourceId(), newStatus);
// 通知上传者审核结果(消息推送)
// notifyUploader(request.getResourceId(), request.getAction(), request.getComment());
Map<String, Object> result = new HashMap<>();
result.put("code", 0);
result.put("message", "审核操作成功");
result.put("data", Map.of(
"resource_id", request.getResourceId(),
"new_status", newStatus,
"audit_record_id", record.getId()
));
return result;
}
/**
* 批量审核
*
* @param resourceIds 资源ID列表
* @param action 审核动作
* @param comment 审核意见
* @param auditorId 审核人
* @return 批量审核结果
*/
public Map<String, Object> batchAudit(
List<String> resourceIds,
AuditAction action,
String comment,
String auditorId
) {
logger.info(String.format(
"批量审核: count=%d, action=%s", resourceIds.size(), action
));
int successCount = 0;
int failCount = 0;
List<String> failedIds = new ArrayList<>();
for (String resourceId : resourceIds) {
try {
AuditRequest request = new AuditRequest();
request.setResourceId(resourceId);
request.setAction(action);
request.setComment(comment);
request.setAuditorId(auditorId);
Map<String, Object> result = performAudit(request);
if ((int) result.get("code") == 0) {
successCount++;
} else {
failCount++;
failedIds.add(resourceId);
}
} catch (Exception e) {
failCount++;
failedIds.add(resourceId);
logger.warning("批量审核失败: resource=" + resourceId + ", error=" + e.getMessage());
}
}
Map<String, Object> result = new HashMap<>();
result.put("code", 0);
result.put("data", Map.of(
"total", resourceIds.size(),
"success", successCount,
"failed", failCount,
"failed_ids", failedIds
));
return result;
}
/**
* AI自动预审
*
* 在人工审核前,自动进行内容安全检测:
* - 文本内容是否包含违禁词
* - 图片是否包含不当内容
* - 文件格式是否合规
* - 文件大小是否超限
*
* @param resourceId 资源ID
* @return 预审结果
*/
public PreAuditResult performPreAudit(String resourceId) {
logger.info("AI预审: resource=" + resourceId);
PreAuditResult result = new PreAuditResult(true, 1.0);
// 1. 文件格式和大小检查
// ResourceMetadata resource = resourceMapper.selectById(resourceId);
// if (resource.getFileSize() > MAX_FILE_SIZE) {
// result = new PreAuditResult(false, 0.0);
// result.addWarning("文件大小超过限制");
// return result;
// }
// 2. 文本内容安全检测(提取PDF/PPT中的文字进行违禁词检查)
// String textContent = extractTextContent(resource.getFileKey());
// ContentSafetyResult textSafety = contentSafetyApi.checkText(textContent);
// if (!textSafety.isSafe()) {
// result.addWarning("文本内容包含敏感词: " + textSafety.getDetails());
// }
// 3. 图片内容安全检测(提取文档中的图片进行AI审核)
// List<byte[]> images = extractImages(resource.getFileKey());
// for (byte[] image : images) {
// ImageSafetyResult imageSafety = contentSafetyApi.checkImage(image);
// if (!imageSafety.isSafe()) {
// result.addWarning("图片内容不合规: " + imageSafety.getCategory());
// }
// }
// 综合评分
if (!result.getWarnings().isEmpty()) {
double penalty = result.getWarnings().size() * 0.2;
double finalScore = Math.max(0.0, 1.0 - penalty);
result = new PreAuditResult(finalScore >= 0.6, finalScore);
}
logger.info(String.format(
"预审完成: resource=%s, safe=%b, score=%.2f",
resourceId, result.isSafe(), result.getSafeScore()
));
return result;
}
/**
* 查询审核记录列表
*
* @param resourceId 资源ID(可选,为空则查所有)
* @param auditorId 审核人ID(可选)
* @param page 页码
* @param pageSize 每页大小
* @return 审核记录列表
*/
public Map<String, Object> queryAuditRecords(
String resourceId,
String auditorId,
int page,
int pageSize
) {
logger.info(String.format(
"查询审核记录: resource=%s, auditor=%s, page=%d",
resourceId, auditorId, page
));
// List<AuditRecord> records = auditRecordMapper.selectByCondition(
// resourceId, auditorId, page, pageSize
// );
// int total = auditRecordMapper.countByCondition(resourceId, auditorId);
Map<String, Object> result = new HashMap<>();
result.put("code", 0);
result.put("data", Map.of(
"total", 0,
"page", page,
"items", new ArrayList<>()
));
return result;
}
/**
* 获取待审核资源数量(仪表盘统计用)
*/
public Map<String, Object> getAuditStats() {
// int pendingCount = resourceMapper.countByStatus("PENDING");
// int approvedToday = auditRecordMapper.countTodayByAction("APPROVE");
// int rejectedToday = auditRecordMapper.countTodayByAction("REJECT");
Map<String, Object> result = new HashMap<>();
result.put("code", 0);
result.put("data", Map.of(
"pending_count", 0,
"approved_today", 0,
"rejected_today", 0
));
return result;
}
}
@@ -0,0 +1,333 @@
/*
* 自然写教学资源管理与内容分发系统软件 V1.0
* service/CdnService.java - CDN分发与缓存管理服务
*/
package com.writech.resource.service;
import java.util.*;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* CDN分发与缓存管理服务
*
* 负责教学资源的CDN加速分发,包括:
* - 签名URL生成(防盗链)
* - CDN缓存预热与刷新
* - 资源分发策略管理
* - 下载流量统计
*/
public class CdnService {
private static final Logger logger =
Logger.getLogger(CdnService.class.getName());
// ============================================================
// CDN配置
// ============================================================
/** CDN域名 */
private static final String CDN_DOMAIN = "https://cdn.writech.com";
/** CDN签名密钥 */
private String cdnSignKey;
/** 签名URL默认有效期(秒) */
private static final int DEFAULT_EXPIRE_SECONDS = 1800;
/** Referer白名单 */
private static final Set<String> REFERER_WHITELIST = new HashSet<>(Arrays.asList(
"*.writech.com",
"localhost",
"127.0.0.1"
));
/** CDN缓存策略(按资源类型配置TTL) */
private static final Map<String, Integer> CACHE_TTL_MAP = new HashMap<>();
static {
CACHE_TTL_MAP.put("pdf", 86400 * 30); // PDF资源缓存30天
CACHE_TTL_MAP.put("image", 86400 * 90); // 图片缓存90天
CACHE_TTL_MAP.put("video", 86400 * 7); // 视频缓存7天
CACHE_TTL_MAP.put("template", 86400 * 30); // 模板缓存30天
CACHE_TTL_MAP.put("dotcode", 86400 * 365); // 点阵码缓存1年(不变内容)
}
public CdnService(String signKey) {
this.cdnSignKey = signKey;
logger.info("CDN服务初始化: domain=" + CDN_DOMAIN);
}
// ============================================================
// 签名URL生成(防盗链核心)
// ============================================================
/**
* 生成CDN签名下载URL
*
* 签名算法(TypeA鉴权):
* 1. 计算签名原文:path-timestamp-rand-uid
* 2. HMAC-SHA256(原文, 密钥)
* 3. 拼接签名URLdomain/path?auth_key=timestamp-rand-uid-signature
*
* @param objectKey OSS对象Key
* @param expireSeconds 有效期(秒)
* @return 签名后的CDN下载URL
*/
public String generateSignedUrl(String objectKey, int expireSeconds) {
if (expireSeconds <= 0) {
expireSeconds = DEFAULT_EXPIRE_SECONDS;
}
long timestamp = System.currentTimeMillis() / 1000 + expireSeconds;
String rand = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
String uid = "0"; // 用户标识(可选)
String path = "/" + objectKey;
// 签名原文
String signContent = String.format("%s-%d-%s-%s", path, timestamp, rand, uid);
// HMAC-SHA256计算签名
String signature = hmacSha256(signContent, cdnSignKey);
// 拼接签名URL
String authKey = String.format("%d-%s-%s-%s", timestamp, rand, uid, signature);
String signedUrl = String.format("%s%s?auth_key=%s", CDN_DOMAIN, path, authKey);
logger.info(String.format(
"生成签名URL: key=%s, expire=%ds", objectKey, expireSeconds
));
return signedUrl;
}
/**
* 验证签名URL是否有效
*
* @param url 待验证的URL
* @return 验证结果
*/
public boolean verifySignedUrl(String url) {
try {
// 解析auth_key参数
String authKey = extractParam(url, "auth_key");
if (authKey == null) return false;
String[] parts = authKey.split("-", 4);
if (parts.length != 4) return false;
long timestamp = Long.parseLong(parts[0]);
String rand = parts[1];
String uid = parts[2];
String receivedSignature = parts[3];
// 检查是否过期
if (System.currentTimeMillis() / 1000 > timestamp) {
return false;
}
// 重新计算签名对比
String path = extractPath(url);
String signContent = String.format("%s-%d-%s-%s", path, timestamp, rand, uid);
String expectedSignature = hmacSha256(signContent, cdnSignKey);
return expectedSignature.equals(receivedSignature);
} catch (Exception e) {
logger.warning("签名验证异常: " + e.getMessage());
return false;
}
}
/**
* HMAC-SHA256签名计算
*/
private String hmacSha256(String data, String key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"
);
mac.init(secretKey);
byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
// 转换为十六进制字符串
StringBuilder sb = new StringBuilder();
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("HMAC-SHA256计算失败", e);
}
}
// ============================================================
// CDN缓存管理
// ============================================================
/**
* 预热CDN缓存
*
* 将指定资源推送到CDN所有边缘节点,确保用户首次访问也能快速响应。
* 通常在资源审核通过后触发预热。
*
* @param objectKeys 要预热的资源Key列表
*/
public void preheatResources(List<String> objectKeys) {
logger.info(String.format("CDN缓存预热: %d个资源", objectKeys.size()));
List<String> urls = new ArrayList<>();
for (String key : objectKeys) {
urls.add(CDN_DOMAIN + "/" + key);
}
// 调用CDN API预热
// PushObjectCacheRequest request = new PushObjectCacheRequest();
// request.setObjectPath(String.join("\n", urls));
// cdnClient.pushObjectCache(request);
logger.info("CDN预热任务已提交");
}
/**
* 刷新CDN缓存
*
* 资源更新或删除后,需要刷新CDN缓存使旧版本失效。
*
* @param objectKeys 要刷新的资源Key列表
*/
public void refreshCache(List<String> objectKeys) {
logger.info(String.format("CDN缓存刷新: %d个资源", objectKeys.size()));
List<String> urls = new ArrayList<>();
for (String key : objectKeys) {
urls.add(CDN_DOMAIN + "/" + key);
}
// 调用CDN API刷新
// RefreshObjectCachesRequest request = new RefreshObjectCachesRequest();
// request.setObjectPath(String.join("\n", urls));
// cdnClient.refreshObjectCaches(request);
logger.info("CDN刷新任务已提交");
}
/**
* 刷新目录缓存(用于整个类别的批量更新)
*/
public void refreshDirectoryCache(String directoryPath) {
logger.info("CDN目录缓存刷新: " + directoryPath);
// RefreshObjectCachesRequest request = new RefreshObjectCachesRequest();
// request.setObjectPath(CDN_DOMAIN + "/" + directoryPath);
// request.setObjectType("Directory");
// cdnClient.refreshObjectCaches(request);
}
// ============================================================
// Referer防盗链校验
// ============================================================
/**
* 校验请求Referer是否在白名单中
*
* @param referer 请求头中的Referer
* @return 是否允许访问
*/
public boolean validateReferer(String referer) {
if (referer == null || referer.isEmpty()) {
return false; // 空Referer拒绝
}
for (String pattern : REFERER_WHITELIST) {
if (pattern.startsWith("*.")) {
// 通配符匹配
String domain = pattern.substring(2);
if (referer.contains(domain)) {
return true;
}
} else {
if (referer.contains(pattern)) {
return true;
}
}
}
logger.warning("Referer校验失败: " + referer);
return false;
}
// ============================================================
// 流量统计
// ============================================================
/**
* 记录资源下载事件(异步写入ClickHouse)
*
* @param resourceId 资源ID
* @param userId 下载用户ID
* @param terminal 终端类型(pad/pc/mobile/board
* @param fileSize 文件大小(字节)
*/
public void recordDownloadEvent(
String resourceId,
String userId,
String terminal,
long fileSize
) {
// 异步写入ClickHouse使用统计表
// Map<String, Object> event = new HashMap<>();
// event.put("resource_id", resourceId);
// event.put("user_id", userId);
// event.put("terminal", terminal);
// event.put("file_size", fileSize);
// event.put("download_at", new Date());
// event.put("cdn_node", getCdnNodeId());
// clickhouseClient.insert("usage_stat", event);
}
/**
* 查询资源下载统计
*/
public Map<String, Object> getDownloadStats(
String resourceId, String startDate, String endDate
) {
// 从ClickHouse查询聚合统计
// SELECT count() as downloads, sum(file_size) as total_bytes,
// uniq(user_id) as unique_users
// FROM usage_stat
// WHERE resource_id = ? AND download_at BETWEEN ? AND ?
Map<String, Object> stats = new HashMap<>();
stats.put("resource_id", resourceId);
stats.put("total_downloads", 0);
stats.put("total_bytes", 0L);
stats.put("unique_users", 0);
stats.put("by_terminal", new HashMap<>());
stats.put("daily_trend", new ArrayList<>());
return stats;
}
// ============================================================
// 辅助方法
// ============================================================
/** 从URL中提取指定参数 */
private String extractParam(String url, String paramName) {
int start = url.indexOf(paramName + "=");
if (start < 0) return null;
start += paramName.length() + 1;
int end = url.indexOf("&", start);
return end > 0 ? url.substring(start, end) : url.substring(start);
}
/** 从URL中提取路径部分 */
private String extractPath(String url) {
int start = url.indexOf("/", url.indexOf("//") + 2);
int end = url.indexOf("?");
return end > 0 ? url.substring(start, end) : url.substring(start);
}
}
@@ -0,0 +1,374 @@
/*
* 自然写教学资源管理与内容分发系统软件 V1.0
* service/DotCodeService.java - 点阵码生成引擎服务
*/
package com.writech.resource.service;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 点阵码生成引擎服务
*
* 负责点阵码资源的生成、分配和管理。
* 点阵码是自然写系统的核心技术,每个点阵码对应一个唯一的
* 页面/区域标识,配合点阵笔可精确定位书写位置。
*
* 功能:
* - 点阵码ID全局唯一分配(防冲突)
* - 点阵码图案生成(OGP编码)
* - 点阵码与页面/课件的绑定关系管理
* - 批量生成点阵码资源包
* - 点阵码PDF合成(叠加到字帖/试卷模板上)
*/
public class DotCodeService {
private static final Logger logger =
Logger.getLogger(DotCodeService.class.getName());
// ============================================================
// 点阵码常量与配置
// ============================================================
/** OGP点阵码编码参数 */
private static final int DOT_GRID_SIZE = 6; // 每组点阵6x6
private static final double DOT_SPACING_MM = 0.3; // 点间距0.3mm
private static final double DOT_OFFSET_MM = 0.1; // 点偏移量0.1mm
private static final int DOTS_PER_PAGE = 10000; // 每页约10000个点
/** 点阵码ID分配范围 */
private static final long ID_RANGE_START = 1_000_000_000L;
private static final long ID_RANGE_END = 9_999_999_999L;
/** 当前已分配的最大ID(原子操作保证线程安全) */
private long currentMaxId = ID_RANGE_START;
/** 点阵码-页面绑定关系缓存 */
private final Map<Long, DotCodeBinding> bindingCache = new ConcurrentHashMap<>();
// ============================================================
// 数据模型
// ============================================================
/** 点阵码绑定关系 */
public static class DotCodeBinding {
private long dotCodeId; // 点阵码ID
private String resourceId; // 绑定的资源ID
private int pageIndex; // 页面序号
private String areaType; // 区域类型(full_page/answer_area/title_area
private double areaX; // 区域起始X坐标(mm
private double areaY; // 区域起始Y坐标(mm
private double areaWidth; // 区域宽度(mm
private double areaHeight; // 区域高度(mm
private Date createdAt;
public DotCodeBinding() {}
public DotCodeBinding(long dotCodeId, String resourceId, int pageIndex) {
this.dotCodeId = dotCodeId;
this.resourceId = resourceId;
this.pageIndex = pageIndex;
this.createdAt = new Date();
}
public long getDotCodeId() { return dotCodeId; }
public void setDotCodeId(long id) { this.dotCodeId = id; }
public String getResourceId() { return resourceId; }
public void setResourceId(String rid) { this.resourceId = rid; }
public int getPageIndex() { return pageIndex; }
public void setPageIndex(int idx) { this.pageIndex = idx; }
public String getAreaType() { return areaType; }
public void setAreaType(String type) { this.areaType = type; }
public double getAreaX() { return areaX; }
public double getAreaY() { return areaY; }
public double getAreaWidth() { return areaWidth; }
public double getAreaHeight() { return areaHeight; }
}
/** 点阵码生成请求 */
public static class DotCodeGenerateRequest {
private String resourceId; // 关联资源ID
private int pageCount; // 页数
private double pageWidth; // 页面宽度(mm
private double pageHeight; // 页面高度(mm
private String outputFormat; // 输出格式(pdf/png/svg
private boolean overlayOnTemplate; // 是否叠加到模板上
private String templateFileKey; // 模板文件OSS Key
public String getResourceId() { return resourceId; }
public void setResourceId(String id) { this.resourceId = id; }
public int getPageCount() { return pageCount; }
public void setPageCount(int count) { this.pageCount = count; }
public double getPageWidth() { return pageWidth > 0 ? pageWidth : 210.0; }
public double getPageHeight() { return pageHeight > 0 ? pageHeight : 297.0; }
public String getOutputFormat() { return outputFormat != null ? outputFormat : "pdf"; }
public boolean isOverlayOnTemplate() { return overlayOnTemplate; }
public String getTemplateFileKey() { return templateFileKey; }
}
/** 点阵码生成结果 */
public static class DotCodeGenerateResult {
private String taskId;
private String resourceId;
private List<Long> dotCodeIds; // 分配的点阵码ID列表
private String outputFileKey; // 生成的文件OSS Key
private int pageCount;
private long totalDots;
private String status; // processing/completed/failed
public String getTaskId() { return taskId; }
public void setTaskId(String id) { this.taskId = id; }
public List<Long> getDotCodeIds() { return dotCodeIds; }
public void setDotCodeIds(List<Long> ids) { this.dotCodeIds = ids; }
public String getOutputFileKey() { return outputFileKey; }
public void setOutputFileKey(String key) { this.outputFileKey = key; }
public String getStatus() { return status; }
public void setStatus(String s) { this.status = s; }
}
// ============================================================
// 核心方法实现
// ============================================================
/**
* 批量生成点阵码资源包 POST /api/v1/dotcode/generate
*
* 流程:
* 1. 分配全局唯一的点阵码ID范围
* 2. 为每页生成OGP编码的点阵图案
* 3. 如果需要叠加模板,合成到模板PDF上
* 4. 上传生成结果到OSS
* 5. 记录绑定关系到MySQL
*/
public DotCodeGenerateResult generateDotCodes(DotCodeGenerateRequest request) {
logger.info(String.format(
"生成点阵码: resource=%s, pages=%d, size=%.0fx%.0fmm",
request.getResourceId(), request.getPageCount(),
request.getPageWidth(), request.getPageHeight()
));
DotCodeGenerateResult result = new DotCodeGenerateResult();
result.setTaskId(UUID.randomUUID().toString().replace("-", "").substring(0, 16));
result.setStatus("processing");
// 1. 分配点阵码ID
List<Long> allocatedIds = allocateDotCodeIds(request.getPageCount());
result.setDotCodeIds(allocatedIds);
// 2. 为每页生成点阵码图案
for (int i = 0; i < request.getPageCount(); i++) {
long dotCodeId = allocatedIds.get(i);
// 生成OGP编码点阵图案
byte[][] dotPattern = generateOGPPattern(
dotCodeId,
request.getPageWidth(),
request.getPageHeight()
);
// 记录绑定关系
DotCodeBinding binding = new DotCodeBinding(
dotCodeId, request.getResourceId(), i
);
binding.setAreaType("full_page");
binding.setAreaX(0);
binding.setAreaY(0);
binding.setAreaWidth(request.getPageWidth());
binding.setAreaHeight(request.getPageHeight());
bindingCache.put(dotCodeId, binding);
// 持久化到MySQL
// dotCodeMapper.insertBinding(binding);
}
// 3. 如果叠加模板,合成PDF
if (request.isOverlayOnTemplate() && request.getTemplateFileKey() != null) {
// 下载模板PDF
// byte[] templatePdf = ossClient.getObject(request.getTemplateFileKey());
// 叠加点阵码图层
// byte[] mergedPdf = pdfMerger.overlayDotCodes(templatePdf, dotPatterns);
// 上传合成后的PDF
// String outputKey = ossClient.putObject(mergedPdf, ...);
// result.setOutputFileKey(outputKey);
}
result.setStatus("completed");
result.setPageCount(request.getPageCount());
result.setTotalDots((long) request.getPageCount() * DOTS_PER_PAGE);
logger.info(String.format(
"点阵码生成完成: task=%s, ids=[%d~%d], dots=%d",
result.getTaskId(),
allocatedIds.get(0),
allocatedIds.get(allocatedIds.size() - 1),
result.getTotalDots()
));
return result;
}
/**
* 分配全局唯一的点阵码ID
*
* 使用原子递增方式保证ID全局唯一,防止多服务器实例间冲突。
* 生产环境使用Redis分布式ID生成器。
*
* @param count 需要分配的ID数量
* @return 分配的ID列表
*/
public synchronized List<Long> allocateDotCodeIds(int count) {
List<Long> ids = new ArrayList<>();
if (currentMaxId + count > ID_RANGE_END) {
throw new RuntimeException("点阵码ID已耗尽,请联系管理员扩容");
}
for (int i = 0; i < count; i++) {
currentMaxId++;
ids.add(currentMaxId);
}
// 持久化当前最大ID(Redis或数据库)
// redisTemplate.set("dot_code_max_id", String.valueOf(currentMaxId));
logger.info(String.format(
"分配点阵码ID: count=%d, range=[%d, %d]",
count, ids.get(0), ids.get(ids.size() - 1)
));
return ids;
}
/**
* 生成OGP编码的点阵图案
*
* OGPOptical Glyph Pattern)编码原理:
* 将点阵码ID编码为点的微小位移方向(上下左右4个方向),
* 每组6x6点阵编码一组信息,整页覆盖实现全页面位置编码。
*
* @param dotCodeId 点阵码ID
* @param pageWidthMm 页面宽度(毫米)
* @param pageHeightMm 页面高度(毫米)
* @return 点阵图案(2D数组,0=无偏移, 1=上, 2=右, 3=下, 4=左)
*/
public byte[][] generateOGPPattern(
long dotCodeId,
double pageWidthMm,
double pageHeightMm
) {
// 计算网格尺寸
int gridCols = (int) (pageWidthMm / DOT_SPACING_MM);
int gridRows = (int) (pageHeightMm / DOT_SPACING_MM);
byte[][] pattern = new byte[gridRows][gridCols];
// 将点阵码ID编码为二进制位流
long encodedId = dotCodeId;
byte[] idBits = new byte[40]; // 40位足以表示10位十进制数
for (int i = 0; i < 40; i++) {
idBits[i] = (byte) ((encodedId >> (39 - i)) & 1);
}
// 填充点阵图案
for (int row = 0; row < gridRows; row++) {
for (int col = 0; col < gridCols; col++) {
// 每个点的偏移方向由其位置和ID编码共同决定
int groupRow = row / DOT_GRID_SIZE;
int groupCol = col / DOT_GRID_SIZE;
int localRow = row % DOT_GRID_SIZE;
int localCol = col % DOT_GRID_SIZE;
// 位置编码 + ID编码 混合
int bitIndex = ((groupRow * (gridCols / DOT_GRID_SIZE) + groupCol)
* DOT_GRID_SIZE * DOT_GRID_SIZE
+ localRow * DOT_GRID_SIZE + localCol) % 40;
// 偏移方向:0=无, 1=上, 2=右, 3=下, 4=左
int positionHash = (row * 7 + col * 13 + (int) dotCodeId) % 5;
pattern[row][col] = (byte) ((positionHash + idBits[bitIndex]) % 5);
}
}
// 添加校验码区域(边缘4行/列作为同步标记和校验)
addSyncMarkers(pattern, gridRows, gridCols);
return pattern;
}
/**
* 在点阵图案边缘添加同步标记和校验码
* 摄像头采集后需要同步标记来确定方向和位置
*/
private void addSyncMarkers(byte[][] pattern, int rows, int cols) {
// 顶部同步行:交替0和1
for (int col = 0; col < cols; col++) {
pattern[0][col] = (byte) (col % 2 == 0 ? 1 : 3);
pattern[1][col] = (byte) (col % 2 == 0 ? 3 : 1);
}
// 左侧同步列
for (int row = 0; row < rows; row++) {
pattern[row][0] = (byte) (row % 2 == 0 ? 2 : 4);
pattern[row][1] = (byte) (row % 2 == 0 ? 4 : 2);
}
// 右下角放置4x4校验码块
// 校验码 = CRC-8(页面ID的低8位)
// 用于摄像头快速验证解码是否正确
}
/**
* 根据点阵码ID查询绑定的资源和页面信息
*
* @param dotCodeId 点阵码ID
* @return 绑定关系(如果存在)
*/
public DotCodeBinding queryBinding(long dotCodeId) {
// 先查缓存
DotCodeBinding cached = bindingCache.get(dotCodeId);
if (cached != null) {
return cached;
}
// 缓存未命中,查数据库
// DotCodeBinding binding = dotCodeMapper.selectByDotCodeId(dotCodeId);
// if (binding != null) {
// bindingCache.put(dotCodeId, binding);
// }
// return binding;
return null;
}
/**
* 查询资源关联的所有点阵码
*/
public List<DotCodeBinding> queryByResourceId(String resourceId) {
// return dotCodeMapper.selectByResourceId(resourceId);
return new ArrayList<>();
}
/**
* 计算点阵码的SHA-256指纹(用于校验完整性)
*/
public String calculatePatternFingerprint(byte[][] pattern) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
for (byte[] row : pattern) {
digest.update(row);
}
byte[] hash = digest.digest();
StringBuilder sb = new StringBuilder();
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256不可用", e);
}
}
}
@@ -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);
}
}
@@ -0,0 +1,231 @@
/*
* 自然写教学资源管理与内容分发系统软件 V1.0
* service/SearchService.java - Elasticsearch全文检索服务
*/
package com.writech.resource.service;
import java.util.*;
import java.util.logging.Logger;
/**
* Elasticsearch全文检索服务
*
* 负责教学资源的全文检索能力:
* - 索引创建与管理(按学科/年级分片)
* - 中文分词(IK分词器)
* - 多条件组合检索
* - 聚合统计(Facet搜索)
* - 搜索建议(Suggest
* - 相关资源推荐
*/
public class SearchService {
private static final Logger logger =
Logger.getLogger(SearchService.class.getName());
/** ES索引名称 */
private static final String INDEX_NAME = "writech_resources";
/** 索引分片数 */
private static final int NUMBER_OF_SHARDS = 3;
/** 索引副本数 */
private static final int NUMBER_OF_REPLICAS = 1;
/** 搜索结果高亮标签 */
private static final String HIGHLIGHT_PRE_TAG = "<em>";
private static final String HIGHLIGHT_POST_TAG = "</em>";
/**
* 创建资源索引(系统初始化时调用)
*
* 索引映射字段:
* - name: text (IK中文分词) + keyword子字段
* - description: text (IK中文分词)
* - tags: keyword数组
* - subject/grade/publisher/type/school_id/audit_status: keyword
* - download_count/use_count: integer
* - created_at/updated_at: date
*/
public void createIndex() {
logger.info("创建ES索引: " + INDEX_NAME);
Map<String, Object> settings = new HashMap<>();
settings.put("number_of_shards", NUMBER_OF_SHARDS);
settings.put("number_of_replicas", NUMBER_OF_REPLICAS);
// IK分词器配置
Map<String, Object> analysis = new HashMap<>();
Map<String, Object> analyzers = new HashMap<>();
analyzers.put("ik_max", Map.of("type", "custom", "tokenizer", "ik_max_word"));
analyzers.put("ik_smart", Map.of("type", "custom", "tokenizer", "ik_smart"));
analysis.put("analyzer", analyzers);
settings.put("analysis", analysis);
// 字段映射定义
Map<String, Object> properties = new LinkedHashMap<>();
// 名称字段:主搜索字段
Map<String, Object> nameField = new HashMap<>();
nameField.put("type", "text");
nameField.put("analyzer", "ik_max_word");
nameField.put("search_analyzer", "ik_smart");
nameField.put("fields", Map.of("keyword", Map.of("type", "keyword")));
properties.put("name", nameField);
// 描述字段
properties.put("description", Map.of("type", "text", "analyzer", "ik_max_word"));
properties.put("tags", Map.of("type", "keyword"));
properties.put("subject", Map.of("type", "keyword"));
properties.put("grade", Map.of("type", "keyword"));
properties.put("publisher", Map.of("type", "keyword"));
properties.put("type", Map.of("type", "keyword"));
properties.put("school_id", Map.of("type", "keyword"));
properties.put("audit_status", Map.of("type", "keyword"));
properties.put("download_count", Map.of("type", "integer"));
properties.put("use_count", Map.of("type", "integer"));
properties.put("created_at", Map.of("type", "date"));
logger.info("ES索引映射已定义: " + properties.size() + "个字段");
}
/**
* 全文检索资源
*
* 搜索策略:
* 1. 关键词multi_match跨name+description+tags字段
* 2. 分类term精确过滤subject/grade/publisher
* 3. 权限过滤(仅审核通过+本校授权)
* 4. 相关性+热度综合排序(function_score
* 5. 聚合统计各分类维度资源数量
* 6. 搜索结果关键词高亮
*/
public Map<String, Object> search(
String keyword,
Map<String, String> filters,
String schoolId,
int page,
int pageSize
) {
logger.info(String.format(
"资源搜索: keyword=%s, school=%s, page=%d", keyword, schoolId, page
));
// 构建Bool查询
// BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键词匹配(boost权重:name:3 > tags:2 > description:1
// if (keyword != null && !keyword.trim().isEmpty()) {
// boolQuery.must(QueryBuilders.multiMatchQuery(keyword)
// .field("name", 3.0f)
// .field("tags", 2.0f)
// .field("description", 1.0f)
// .type(MultiMatchQueryBuilder.Type.BEST_FIELDS)
// .minimumShouldMatch("70%"));
// }
// 分类过滤
// if (filters != null) {
// filters.forEach((key, value) -> {
// if (value != null) boolQuery.filter(termQuery(key, value));
// });
// }
// 权限过滤:仅返回审核通过的资源
// boolQuery.filter(termQuery("audit_status", "APPROVED"));
// boolQuery.filter(termQuery("school_id", schoolId));
// function_score:相关性*0.7 + log(download_count+1)*0.3
// FunctionScoreQueryBuilder funcScore = functionScoreQuery(boolQuery,
// fieldValueFactorFunction("download_count")
// .modifier(Modifier.LOG1P).factor(0.3f)
// ).scoreMode(ScoreMode.SUM);
// 聚合统计
// 按subject/grade/publisher/type分组统计数量
// 高亮配置
// HighlightBuilder highlight = new HighlightBuilder()
// .preTags(HIGHLIGHT_PRE_TAG).postTags(HIGHLIGHT_POST_TAG)
// .field("name").field("description");
Map<String, Object> result = new HashMap<>();
result.put("total", 0);
result.put("page", page);
result.put("items", new ArrayList<>());
result.put("facets", Map.of(
"by_subject", new ArrayList<>(),
"by_grade", new ArrayList<>(),
"by_publisher", new ArrayList<>(),
"by_type", new ArrayList<>()
));
return result;
}
/**
* 搜索建议(输入补全)
* 用户输入时实时返回匹配的资源名称建议
*/
public List<String> suggest(String prefix, int size) {
if (prefix == null || prefix.trim().isEmpty()) {
return Collections.emptyList();
}
logger.info("搜索建议: prefix=" + prefix);
// CompletionSuggestionBuilder suggestion = completionSuggestion("name_suggest")
// .prefix(prefix).size(size);
return new ArrayList<>();
}
/**
* 相关资源推荐(More Like This查询)
* 基于内容相似度推荐同类资源
*/
public List<Map<String, Object>> recommend(String resourceId, int size) {
logger.info(String.format("相关推荐: resource=%s, size=%d", resourceId, size));
// moreLikeThisQuery(["name","description","tags"], null, [item(INDEX, id)])
// .minTermFreq(1).maxQueryTerms(12)
return new ArrayList<>();
}
/** 索引单个资源文档 */
public void indexDocument(String resourceId, Map<String, Object> doc) {
logger.info("索引资源: id=" + resourceId);
}
/** 更新索引文档(部分更新) */
public void updateDocument(String resourceId, Map<String, Object> partialDoc) {
logger.info("更新索引: id=" + resourceId);
}
/** 删除索引文档 */
public void deleteDocument(String resourceId) {
logger.info("删除索引: id=" + resourceId);
}
/**
* 批量重建索引
* 从MySQL全量加载资源元数据,重新构建ES索引
*/
public int rebuildIndex() {
logger.info("开始重建ES索引...");
// 1. 删除旧索引
// 2. 重新创建索引(含映射)
createIndex();
// 3. 从MySQL批量查询所有审核通过的资源
// 4. 使用BulkRequest批量索引
int count = 0;
// List<Resource> allResources = resourceMapper.selectAllApproved();
// BulkRequest bulk = new BulkRequest();
// for (Resource r : allResources) {
// bulk.add(new IndexRequest(INDEX_NAME).id(r.getId()).source(toDoc(r)));
// count++;
// if (count % 500 == 0) {
// elasticsearchClient.bulk(bulk);
// bulk = new BulkRequest();
// }
// }
// if (bulk.numberOfActions() > 0) elasticsearchClient.bulk(bulk);
logger.info("ES索引重建完成: " + count + "");
return count;
}
}