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,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);
}
}
}