package jnpf.dmsService; import cn.hutool.crypto.digest.DigestUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import jnpf.base.UserInfo; import jnpf.dmsEntity.print.DmsPrintOperationEntity; import jnpf.dmsEntity.print.DmsPrintOperationLogEntity; import jnpf.dmsEntity.print.form.DmsPrintOperationCreateForm; import jnpf.dmsEntity.print.form.DmsPrintOperationQuery; import jnpf.dmsEntity.print.form.DmsPrintTargetForm; import jnpf.dmsEntity.print.vo.DmsPrintOperationCreatedVO; import jnpf.dmsEntity.print.vo.DmsPrintOperationLogVO; import jnpf.dmsMapper.DmsPrintOperationLogMapper; import jnpf.dmsMapper.DmsPrintOperationMapper; import jnpf.dmsService.support.DmsPrintOperationRequestContext; import jnpf.exception.DataException; import jnpf.util.RandomUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class DmsPrintOperationLogService { private static final int MAX_TARGETS = 200; private static final List TARGET_TYPES = java.util.Arrays.asList("DOCUMENT", "BUSINESS_RECORD", "BLANK_FORM"); private static final List PRINT_TYPES = java.util.Arrays.asList( "CONTROLLED_DOCUMENT", "RECORD_DOCUMENT", "BLANK_FORM", "LABEL", "ARCHIVE_BOX", "OTHER"); private static final List SOURCE_TYPES = java.util.Arrays.asList("FILE_PREVIEW", "FORM_DETAIL", "LIST_BATCH"); private final DmsPrintOperationMapper operationMapper; private final DmsPrintOperationLogMapper logMapper; private final DmsPrintOperationRequestContext requestContext; @Transactional(rollbackFor = Exception.class) public DmsPrintOperationCreatedVO create(DmsPrintOperationCreateForm form) throws DataException { UserInfo user = requestContext.currentUser(); if (user == null || !StringUtils.hasText(user.getUserId())) { throw new DataException("缺少登录用户信息"); } String clientEventId = uuidText(form == null ? null : form.getClientEventId(), "clientEventId"); String printType = enumText(form.getPrintType(), PRINT_TYPES, "printType"); String sourceType = enumText(form.getSourceType(), SOURCE_TYPES, "sourceType"); int copies = form.getCopies() == null ? 1 : form.getCopies(); if (copies < 1 || copies > 999) { throw new DataException("copies 必须在 1 到 999 之间"); } String remark = optional(form.getRemark(), 500, "remark"); String printName = required(form.getPrintName(), 300, "printName"); List targets = normalizeTargets(form.getTargets(), printName); String requestHash = requestHash(printType, sourceType, copies, remark, targets); Date now = new Date(); DmsPrintOperationEntity operation = new DmsPrintOperationEntity(); operation.setId(RandomUtil.uuId()); operation.setClientEventId(clientEventId); operation.setRequestHash(requestHash); operation.setTargetCount(targets.size()); operation.setCreatedAt(now); if (operationMapper.insertIgnore(operation) == 0) { DmsPrintOperationEntity existing = operationMapper.selectByClientEventId(clientEventId); if (existing == null || !requestHash.equals(existing.getRequestHash())) { throw new DataException("CLIENT_EVENT_CONFLICT:相同 clientEventId 对应了不同打印请求"); } return new DmsPrintOperationCreatedVO(existing.getId(), existing.getTargetCount()); } String menuId = optional(requestContext.header("Jnpf-Menu-Id"), 100, "Jnpf-Menu-Id"); String menuName = menuId == null ? null : logMapper.selectMenuNameById(menuId); String parentId = optional(requestContext.header("Jnpf-Parent-Id"), 100, "Jnpf-Parent-Id"); String parentBizModule = menuId == null || parentId == null ? null : logMapper.selectParentBizModuleName(menuId, parentId); String operatorName = optional(user.getUserName(), 200, "operatorName"); if (operatorName == null) { operatorName = user.getUserId().trim(); } String requestIp = optional(requestContext.requestIp(), 64, "requestIp"); String userAgent = optional(requestContext.userAgent(), 1000, "userAgent"); for (NormalizedTarget target : targets) { DmsPrintOperationLogEntity row = new DmsPrintOperationLogEntity(); row.setId(RandomUtil.uuId()); row.setOperationId(operation.getId()); row.setTargetType(target.targetType); row.setTargetId(target.targetId); row.setDocumentId(target.documentId); row.setDocumentCode(target.documentCode); row.setDocumentName(target.documentName); row.setBatchNo(target.batchNo); row.setVersionNo(target.versionNo); row.setPrintType(printType); row.setCopies(copies); row.setStatus("INITIATED"); row.setSourceType(sourceType); row.setSourceMenuId(menuId); row.setSourceMenuName(menuName); row.setParentBizModule(parentBizModule); row.setOperatorId(user.getUserId().trim()); row.setOperatorName(operatorName); row.setOperatedAt(now); row.setRemark(target.remark == null ? remark : target.remark); row.setRequestIp(requestIp); row.setUserAgent(userAgent); logMapper.insert(row); } return new DmsPrintOperationCreatedVO(operation.getId(), targets.size()); } @Transactional(readOnly = true) public List list(DmsPrintOperationQuery query) throws DataException { DmsPrintOperationQuery q = query == null ? new DmsPrintOperationQuery() : query; if (q.getPageSize() > 200) { throw new DataException("pageSize 不能超过 200"); } Page page = new Page<>(q.getCurrentPage(), q.getPageSize()); Page result = logMapper.selectPage(page, queryWrapper(q)); List values = result.getRecords().stream().map(this::toVO).collect(Collectors.toList()); q.setData(values, result.getTotal()); return values; } @Transactional(readOnly = true) public DmsPrintOperationLogVO detail(String id) throws DataException { DmsPrintOperationLogEntity row = logMapper.selectById(id); if (row == null) { throw new DataException("打印记录不存在"); } return toVO(row); } @Transactional(readOnly = true) public List export(DmsPrintOperationQuery query) { DmsPrintOperationQuery q = query == null ? new DmsPrintOperationQuery() : query; return logMapper.selectList(queryWrapper(q).last("LIMIT 100000")).stream().map(this::toVO).collect(Collectors.toList()); } private LambdaQueryWrapper queryWrapper(DmsPrintOperationQuery q) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); String keyword = trim(q.getKeyword()); if (keyword != null) { wrapper.and(w -> w.like(DmsPrintOperationLogEntity::getDocumentCode, keyword) .or().like(DmsPrintOperationLogEntity::getDocumentName, keyword) .or().like(DmsPrintOperationLogEntity::getBatchNo, keyword) .or().like(DmsPrintOperationLogEntity::getVersionNo, keyword) .or().like(DmsPrintOperationLogEntity::getOperatorName, keyword) .or().like(DmsPrintOperationLogEntity::getRemark, keyword)); } like(wrapper, DmsPrintOperationLogEntity::getDocumentCode, q.getDocumentCode()); like(wrapper, DmsPrintOperationLogEntity::getDocumentName, q.getDocumentName()); like(wrapper, DmsPrintOperationLogEntity::getBatchNo, q.getBatchNo()); like(wrapper, DmsPrintOperationLogEntity::getVersionNo, q.getVersionNo()); like(wrapper, DmsPrintOperationLogEntity::getOperatorName, q.getOperatorName()); if (StringUtils.hasText(q.getPrintType())) wrapper.eq(DmsPrintOperationLogEntity::getPrintType, q.getPrintType().trim()); if (StringUtils.hasText(q.getOperatorId())) wrapper.eq(DmsPrintOperationLogEntity::getOperatorId, q.getOperatorId().trim()); if (q.getStartTime() != null) wrapper.ge(DmsPrintOperationLogEntity::getOperatedAt, q.getStartTime()); if (q.getEndTime() != null) wrapper.lt(DmsPrintOperationLogEntity::getOperatedAt, q.getEndTime()); return wrapper.orderByDesc(DmsPrintOperationLogEntity::getOperatedAt) .orderByDesc(DmsPrintOperationLogEntity::getId); } private void like(LambdaQueryWrapper wrapper, com.baomidou.mybatisplus.core.toolkit.support.SFunction column, String value) { if (StringUtils.hasText(value)) wrapper.like(column, value.trim()); } private List normalizeTargets(List raw, String printName) throws DataException { if (raw == null || raw.isEmpty() || raw.size() > MAX_TARGETS) { throw new DataException("targets 数量必须在 1 到 " + MAX_TARGETS + " 之间"); } Map unique = new LinkedHashMap<>(); for (DmsPrintTargetForm value : raw) { if (value == null) throw new DataException("targets 不能包含空项"); NormalizedTarget target = new NormalizedTarget(); target.targetType = enumText(value.getTargetType(), TARGET_TYPES, "targetType"); target.targetId = required(value.getTargetId(), 200, "targetId"); target.documentId = value.getDocumentId(); target.documentCode = optional(value.getDocumentCode(), 300, "documentCode"); target.documentName = optional(value.getDocumentName(), 300, "documentName"); target.batchNo = optional(value.getBatchNo(), 200, "batchNo"); target.versionNo = optional(value.getVersionNo(), 100, "versionNo"); target.remark = optional(value.getRemark(), 500, "target.remark"); if (target.documentCode == null) target.documentCode = target.targetId; if (target.documentName == null) target.documentName = printName; unique.putIfAbsent(target.targetType + ':' + target.targetId, target); } return new ArrayList<>(unique.values()); } private String requestHash(String printType, String sourceType, int copies, String remark, List targets) { StringBuilder value = new StringBuilder(); append(value, printType); append(value, sourceType); append(value, String.valueOf(copies)); append(value, remark); targets.stream().sorted((a, b) -> (a.targetType + a.targetId).compareTo(b.targetType + b.targetId)).forEach(t -> { append(value, t.targetType); append(value, t.targetId); append(value, t.documentCode); append(value, t.documentName); append(value, t.batchNo); append(value, t.versionNo); append(value, t.remark); }); return DigestUtil.sha256Hex(value.toString()); } private void append(StringBuilder value, String item) { String text = item == null ? "" : item; value.append(text.length()).append(':').append(text).append('|'); } private DmsPrintOperationLogVO toVO(DmsPrintOperationLogEntity row) { DmsPrintOperationLogVO vo = new DmsPrintOperationLogVO(); org.springframework.beans.BeanUtils.copyProperties(row, vo); return vo; } private String enumText(String value, List allowed, String name) throws DataException { String normalized = required(value, 50, name).toUpperCase(); if (!allowed.contains(normalized)) throw new DataException(name + " 取值无效"); return normalized; } private String uuidText(String value, String name) throws DataException { String normalized = required(value, 50, name); try { return UUID.fromString(normalized).toString(); } catch (IllegalArgumentException ex) { throw new DataException(name + " 必须是 UUID"); } } private String required(String value, int max, String name) throws DataException { String normalized = optional(value, max, name); if (normalized == null) throw new DataException(name + " 不能为空"); return normalized; } private String optional(String value, int max, String name) throws DataException { String normalized = trim(value); if (normalized != null && normalized.length() > max) throw new DataException(name + " 长度不能超过 " + max); return normalized; } private String trim(String value) { return value == null || value.trim().isEmpty() ? null : value.trim(); } private static class NormalizedTarget { private String targetType; private String targetId; private String documentId; private String documentCode; private String documentName; private String batchNo; private String versionNo; private String remark; } }