package jnpf.bizcommon.onlyoffice.service.impl; import cn.hutool.core.util.IdUtil; import cn.hutool.crypto.digest.DigestUtil; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeSessionEntity; import jnpf.bizcommon.onlyoffice.mapper.OnlyOfficeSessionMapper; import jnpf.exception.DataException; import lombok.RequiredArgsConstructor; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.UUID; /** 在 OnlyOffice 会话状态机内为 ELN 模板发布提供互斥 fence。 */ @Service @RequiredArgsConstructor public class OnlyOfficeTemplatePublishFenceService { static final String ELN_TEMPLATE_BIZ_MODULE = "eln.template"; private final OnlyOfficeSessionMapper sessionMapper; public String acquire(String tenantId, String templateId, String fileId, String fileType) { requireIdentity(tenantId, templateId, fileId, fileType); String token = UUID.randomUUID().toString(); OnlyOfficeSessionEntity existing = find(tenantId, fileId, fileType); if (existing != null) { acquireExisting(existing, tenantId, templateId, fileId, fileType, token); return token; } OnlyOfficeSessionEntity placeholder = new OnlyOfficeSessionEntity(); placeholder.setId(IdUtil.getSnowflakeNextIdStr()); placeholder.setTenantId(tenantId); placeholder.setFileId(fileId); placeholder.setFileType(fileType); placeholder.setFileName(fileId); placeholder.setDocumentType("word"); placeholder.setBizModule(ELN_TEMPLATE_BIZ_MODULE); placeholder.setBizDataId(templateId); placeholder.setVersionNo(1); placeholder.setVersion(1); placeholder.setSessionStatus(OnlyOfficeSessionEntity.SessionStatus.PUBLISHING); placeholder.setPublishToken(token); placeholder.setDocKey(DigestUtil.md5Hex(fileId + "_1")); try { if (sessionMapper.insert(placeholder) == 1) { return token; } } catch (DuplicateKeyException ignored) { // 编辑器首次打开与发布同时插入时,由文件业务唯一键裁决,再重读走同一 CAS。 } existing = find(tenantId, fileId, fileType); if (existing == null) { throw busy(); } acquireExisting(existing, tenantId, templateId, fileId, fileType, token); return token; } public boolean release(String tenantId, String templateId, String fileId, String fileType, String token) { requireIdentity(tenantId, templateId, fileId, fileType); if (!StringUtils.hasText(token)) { throw new DataException("发布 fence token 不能为空"); } return sessionMapper.update(null, identityUpdate( tenantId, templateId, fileId, fileType) .eq("session_status", OnlyOfficeSessionEntity.SessionStatus.PUBLISHING) .eq("publish_token", token) .set("session_status", OnlyOfficeSessionEntity.SessionStatus.IDLE) .set("publish_token", null)) == 1; } private void acquireExisting(OnlyOfficeSessionEntity existing, String tenantId, String templateId, String fileId, String fileType, String token) { if (!ELN_TEMPLATE_BIZ_MODULE.equals(existing.getBizModule()) || !templateId.equals(existing.getBizDataId()) || !Integer.valueOf(OnlyOfficeSessionEntity.SessionStatus.IDLE) .equals(existing.getSessionStatus())) { throw busy(); } int nextVersion = existing.getVersion() == null ? 1 : existing.getVersion() + 1; UpdateWrapper update = identityUpdate( tenantId, templateId, fileId, fileType) .eq("f_id", existing.getId()) .eq("doc_key", existing.getDocKey()) .eq("version_no", existing.getVersionNo()) .eq("session_status", OnlyOfficeSessionEntity.SessionStatus.IDLE) .set("session_status", OnlyOfficeSessionEntity.SessionStatus.PUBLISHING) .set("publish_token", token) .set("lock_user_id", null) .set("lock_heartbeat", null) .set("f_version", nextVersion); if (existing.getVersion() == null) { update.isNull("f_version"); } else { update.eq("f_version", existing.getVersion()); } if (sessionMapper.update(null, update) != 1) { throw busy(); } } private OnlyOfficeSessionEntity find(String tenantId, String fileId, String fileType) { return sessionMapper.selectOne(Wrappers.lambdaQuery() .eq(OnlyOfficeSessionEntity::getTenantId, tenantId) .eq(OnlyOfficeSessionEntity::getFileId, fileId) .eq(OnlyOfficeSessionEntity::getFileType, fileType) .last("limit 1")); } private UpdateWrapper identityUpdate( String tenantId, String templateId, String fileId, String fileType) { return Wrappers.update() .eq("f_tenant_id", tenantId) .eq("file_id", fileId) .eq("file_type", fileType) .eq("biz_module", ELN_TEMPLATE_BIZ_MODULE) .eq("biz_data_id", templateId) .and(active -> active.isNull("f_delete_mark").or().eq("f_delete_mark", 0)); } private static void requireIdentity( String tenantId, String templateId, String fileId, String fileType) { if (!StringUtils.hasText(tenantId) || !StringUtils.hasText(templateId) || !StringUtils.hasText(fileId) || !StringUtils.hasText(fileType)) { throw new DataException("模板发布 fence 身份参数不完整"); } } private static DataException busy() { return new DataException("模板文档正在编辑、保存或发布中,请稍后重试"); } }