software copyright
This commit is contained in:
@@ -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编码的点阵图案
|
||||
*
|
||||
* OGP(Optical 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user