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

375 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/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);
}
}
}