/* * 自然写教学资源管理与内容分发系统软件 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 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 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 getDotCodeIds() { return dotCodeIds; } public void setDotCodeIds(List 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 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 allocateDotCodeIds(int count) { List 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 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); } } }