package jnpf.bizcommon.onlyoffice.service.impl;
|
|
import cn.hutool.core.net.URLEncodeUtil;
|
import cn.hutool.jwt.JWTUtil;
|
import jnpf.base.UserInfo;
|
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeCallbackParam;
|
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeConfigParam;
|
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeProperties;
|
import jnpf.constant.GlobalConst;
|
import jnpf.dmsEntity.compare.DmsCompareTicket;
|
import jnpf.dmsEntity.preview.vo.DmsFilePreviewTicketVO;
|
import jnpf.exception.DataException;
|
import jnpf.util.TicketUtil;
|
import org.springframework.stereotype.Service;
|
import java.nio.charset.StandardCharsets;
|
import java.util.LinkedHashMap;
|
import java.util.Map;
|
import java.util.UUID;
|
|
@Service
|
public class OnlyOfficeCompareService {
|
private final OnlyOfficeProperties properties;
|
private final DmsDocumentCompareConfigBuilder builder;
|
private final DmsOnlyOfficePreviewTicketValidator validator;
|
private final TicketReader tickets;
|
private final OnlyOfficeComparisonRenderer renderer;
|
|
@org.springframework.beans.factory.annotation.Autowired
|
public OnlyOfficeCompareService(OnlyOfficeProperties properties, DmsDocumentCompareConfigBuilder builder,
|
DmsOnlyOfficePreviewTicketValidator validator, OnlyOfficeComparisonRenderer renderer) {
|
this(properties, builder, validator, TicketUtil::parseTicket, renderer);
|
}
|
|
OnlyOfficeCompareService(OnlyOfficeProperties properties, DmsDocumentCompareConfigBuilder builder,
|
DmsOnlyOfficePreviewTicketValidator validator, TicketReader tickets, OnlyOfficeComparisonRenderer renderer) {
|
this.properties = properties;
|
this.builder = builder;
|
this.validator = validator;
|
this.tickets = tickets;
|
this.renderer = renderer;
|
}
|
|
public Map<String, Object> createConfig(String ticketValue, UserInfo user) {
|
DmsCompareTicket ticket;
|
try {
|
ticket = DmsCompareTicket.fromRaw(tickets.read(ticketValue));
|
} catch (RuntimeException e) {
|
throw new DataException("比较会话已过期,请重新选择版本");
|
}
|
String tenant = user == null || user.getTenantId() == null || user.getTenantId().trim().isEmpty()
|
? GlobalConst.DEFAULT_TENANT_VALUE : user.getTenantId().trim();
|
if (user == null || !ticket.getTenantId().equals(tenant) || !ticket.getUserId().equals(user.getUserId())) {
|
throw new DataException("比较票据与当前用户或租户不匹配");
|
}
|
validateSource(ticket.getHistorical(), user);
|
validateSource(ticket.getCurrent(), user);
|
String key = DmsDocumentCompareConfigBuilder.KEY_PREFIX + UUID.randomUUID();
|
String title = ticket.getCurrent().getFileName() + "|" + ticket.getHistoricalLabel() + " → " + ticket.getCurrentLabel();
|
secret(); // Fail before invoking DS if signing is unavailable.
|
String resultPath = renderer.generate(sourceUrl(ticket.getHistorical()), sourceUrl(ticket.getCurrent()));
|
if (ticket.getExpiresAt() <= System.currentTimeMillis()) {
|
throw new DataException("比较会话已过期,请重新选择版本");
|
}
|
Map<String, Object> resultClaims = new LinkedHashMap<>();
|
resultClaims.put("scope", "dms:onlyoffice:compare-result");
|
resultClaims.put("key", key);
|
resultClaims.put("path", resultPath);
|
resultClaims.put("expiresAt", ticket.getExpiresAt());
|
String resultTicket = JWTUtil.createToken(resultClaims, secret());
|
String resultUrl = properties.getFileFetchBaseUrl() + "/api/biz/onlyoffice/compare/result?ticket="
|
+ URLEncodeUtil.encode(resultTicket, GlobalConst.DEFAULT_CHARSET);
|
Map<String, Object> config = builder.comparisonConfig(key, title, resultUrl,
|
properties.getCallbackBaseUrl() + "/api/biz/onlyoffice/compare/callback", user);
|
config.put("token", JWTUtil.createToken(config, secret()));
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("config", config);
|
result.put("title", title);
|
result.put("expiresAt", ticket.getExpiresAt());
|
return result;
|
}
|
|
public byte[] readResult(String ticket) {
|
try {
|
if (ticket == null || !JWTUtil.verify(ticket, secret())) throw new IllegalArgumentException();
|
cn.hutool.json.JSONObject payload = JWTUtil.parseToken(ticket).getPayloads();
|
if (!"dms:onlyoffice:compare-result".equals(payload.getStr("scope"))
|
|| payload.getLong("expiresAt", 0L) <= System.currentTimeMillis()) {
|
throw new IllegalArgumentException();
|
}
|
return renderer.readResult(payload.getStr("path"));
|
} catch (Exception e) {
|
throw new DataException("比较结果票据无效或已过期");
|
}
|
}
|
|
/** No file/session repository or write-back dependency: DS owns and expires its temporary copy. */
|
public void acknowledgeCallback(OnlyOfficeCallbackParam body, String authorization) {
|
String token = authorization == null ? body.getToken()
|
: authorization.replaceFirst("^Bearer ", "").trim();
|
if (token == null || !JWTUtil.verify(token, secret())) throw new DataException("比较回调验签失败");
|
if (body.getKey() == null || !body.getKey().startsWith(DmsDocumentCompareConfigBuilder.KEY_PREFIX)) {
|
throw new DataException("无效的比较会话");
|
}
|
// Intentionally never download body.url or call the normal OnlyOffice save handler.
|
}
|
|
private void validateSource(DmsFilePreviewTicketVO source, UserInfo user) {
|
if (source.getFileName() == null || !source.getFileName().toLowerCase(java.util.Locale.ROOT).endsWith(".docx")) {
|
throw new DataException("版本比较目前仅支持 DOCX 文件");
|
}
|
OnlyOfficeConfigParam param = new OnlyOfficeConfigParam();
|
param.setFileId(source.getFileVersionId().toString());
|
param.setFileName(source.getFileName());
|
param.setDmsAccessTicket(source.getAccessTicket());
|
validator.validate(param, user);
|
}
|
|
private String sourceUrl(DmsFilePreviewTicketVO source) {
|
return properties.getFileFetchBaseUrl() + "/api/file/Dms/OnlyOffice/" + source.getFileVersionId()
|
+ "?ticket=" + URLEncodeUtil.encode(source.getAccessTicket(), GlobalConst.DEFAULT_CHARSET);
|
}
|
|
private byte[] secret() {
|
String value = properties.getJwtSecret();
|
if (value == null || value.trim().isEmpty() || value.contains("${")) {
|
throw new DataException("OnlyOffice 签名密钥未正确配置");
|
}
|
return value.getBytes(StandardCharsets.UTF_8);
|
}
|
|
@FunctionalInterface interface TicketReader { Object read(String ticket); }
|
}
|