package jnpf.flowable.service;
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
import jnpf.dmsBusinessOccupancy.BusinessFileEligibilityService.Operation;
|
import jnpf.dmsBusinessOccupancy.BusinessFileSubmissionService;
|
import jnpf.flowable.mapper.DmsLifecycleNativeFormMapper;
|
import jnpf.exception.WorkFlowException;
|
import jnpf.util.UserProvider;
|
import org.mybatis.spring.SqlSessionTemplate;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.stereotype.Service;
|
import java.util.*;
|
|
/** Only native launch/relaunch, before engine start. Approval writes do not call this method. */
|
@Service
|
public class DmsLifecycleSubmissionGuard implements jnpf.flowable.model.filepermission.DmsRevisionAttachmentCheck {
|
@org.springframework.beans.factory.annotation.Autowired
|
private jnpf.file.FileContentApi fileContentApi;
|
private static final ObjectMapper JSON = new ObjectMapper();
|
// Only approved original versions and immutable DMS edit copies may reuse a comparison.
|
// Ordinary file IDs can be overwritten by legacy editors and are deliberately not cached.
|
private final Map<String, Boolean> contentChecks = Collections.synchronizedMap(new LinkedHashMap<>(128, .75f, true) {
|
@Override protected boolean removeEldestEntry(Map.Entry<String, Boolean> entry) { return size() > 2048; }
|
});
|
private final DmsLifecycleNativeFormMapper mapper;
|
private final BusinessFileSubmissionService submissions;
|
private final jnpf.dmsBusinessOccupancy.BusinessRevisionSubmissionService revisions;
|
private final boolean enabled;
|
private final DmsLifecycleNativeFormAdapter nativeForms;
|
public DmsLifecycleSubmissionGuard(DmsLifecycleNativeFormMapper mapper, SqlSessionTemplate sessions, DmsLifecycleNativeFormAdapter nativeForms,
|
@Value("${dms.business-occupancy.enabled:false}") boolean enabled) {
|
this.mapper=mapper;this.enabled=enabled;this.nativeForms=nativeForms;
|
revisions=enabled ? new jnpf.dmsBusinessOccupancy.BusinessRevisionSubmissionService(sessions) : null;
|
submissions=enabled ? new BusinessFileSubmissionService(sessions) : null;
|
}
|
|
/** Native launch/relaunch entry; the result freezes DMS identity before the engine is started. */
|
public boolean beforeSave(String formId,String taskId,Integer status,Map<String,Object> data) throws WorkFlowException {
|
if(!enabled || (!Integer.valueOf(0).equals(status) && !Integer.valueOf(1).equals(status))) return false;
|
var form=mapper.publishedLifecycleForm(formId);
|
if(form==null) return false;
|
try { nativeForms.select(formId,null,true); }
|
catch (RuntimeException failure) { throw new WorkFlowException(failure.getMessage()); }
|
Operation operation=switch(form.getMainTable()) {
|
case "dms_wenjian_xiuding_shenqing" -> Operation.REVISION;
|
case "dms_wenjian_chexiao_tiyi" -> Operation.REVOCATION;
|
case "dms_wenjian_shixiao_shenqing" -> Operation.INVALIDATION;
|
case "wenjian_fushen_yanqi_shenqing" -> Operation.EXTENSION;
|
default -> null;
|
};
|
try {
|
String tenant=UserProvider.getUser().getTenantId();
|
tenant=tenant==null || tenant.isBlank() ? "0" : tenant;
|
var ids=mapper.applicationIds(tenant,taskId,form.getMainTable());
|
if(ids.size()>1) throw new IllegalStateException("流程任务关联多个生命周期申请,不能选择任意一条");
|
// Native non-incrementing forms initially persist the server-generated task ID as main ID.
|
JsonNode config=JSON.readTree(form.getFormData());
|
if(config.path("primaryKeyPolicy").asInt(1)!=1)
|
throw new IllegalStateException("生命周期申请需要原生非自增主键,不能猜测业务主键");
|
String applicationId=ids.isEmpty()?taskId:ids.get(0);
|
if("dms_wenjian_xinzeng_shenhe".equals(form.getMainTable())) {
|
String numberId=string(data.get("wenjian_bianhao_id"));
|
// Empty initial drafts remain valid; once a number is selected its use is atomic
|
// with native persistence and the existing name-change callback's row lock.
|
String number=numberId==null && status==0 ? null : submissions.guardCreationNumber(
|
tenant,numberId,string(data.get("wenjian_mingcheng")));
|
if(status==0 && !submissions.hasOccupancy(tenant,taskId)) return true;
|
if(number==null || !number.equals(string(data.get("wenjian_bianhao"))))
|
throw new IllegalArgumentException("新增审核编号与已发放编号不一致");
|
List<String> numbers=new ArrayList<>();numbers.add(number);
|
String recordKey=detailKey(config.path("fields"),"dms_wenjian_xinzeng_shenhe_jilu_mingxi");
|
Object records=recordKey==null ? null : data.get(recordKey);
|
if(records!=null && !(records instanceof List<?>)) throw new IllegalArgumentException("新增审核记录明细格式错误");
|
if(records instanceof List<?> list) for(Object raw:list) {
|
if(!(raw instanceof Map<?,?> row)) throw new IllegalArgumentException("新增审核记录明细格式错误");
|
String change=string(row.get("biangeng_fangshi"));
|
if(change!=null && !"xinjian".equals(change)) throw new IllegalArgumentException("新增记录只能选择新建");
|
numbers.add(string(row.get("fujian_bianhao")));
|
}
|
submissions.submitCreation(tenant,applicationId,taskId,numbers,status==0);
|
return true;
|
}
|
if("dms_wenjian_xiuding_shenhe".equals(form.getMainTable())) {
|
if(status==0 && !revisions.hasSubmission(tenant,applicationId,taskId)) return true;
|
String recordKey=detailKey(config.path("fields"),"dms_wenjian_xiuding_shenhe_jilu_mingxi");
|
if(recordKey==null) throw new IllegalArgumentException("修订审核缺少已发布的原生记录子表配置");
|
Object records=data.get(recordKey);
|
if(!(records instanceof List<?> list)) throw new IllegalArgumentException("修订审核必须提交完整记录明细,允许空数组");
|
List<jnpf.dmsBusinessOccupancy.BusinessRevisionRecord> targets=new ArrayList<>();
|
for(Object raw:list) {
|
if(!(raw instanceof Map<?,?> row)) throw new IllegalArgumentException("修订记录明细格式错误");
|
var target=new jnpf.dmsBusinessOccupancy.BusinessRevisionRecord();
|
target.setFileId(string(row.get("jilu_wenjian_id")));
|
target.setBusinessVersionId(string(row.get("yuan_wenjian_banben_id")));
|
target.setFileNumber(string(row.get("fujian_bianhao")));
|
target.setChange(string(row.get("biangeng_fangshi")));targets.add(target);
|
}
|
revisions.submit(tenant,applicationId,taskId,string(data.get("xiuding_shenqing_zixiang_id")),
|
string(data.get("biangeng_fangshi")),targets,status==0);
|
if(status==1) data.put(REQUIRED, true);
|
return true;
|
}
|
if(operation==null) throw new IllegalArgumentException("未知生命周期申请类型");
|
String key=detailKey(config.path("fields"),form.getMainTable()+"_zixiang");
|
Object rows=key==null || data==null ? null : data.get(key);
|
List<BusinessFileSubmissionService.Target> targets=new ArrayList<>();
|
if(rows instanceof List<?> list) for(Object raw:list) {
|
if(!(raw instanceof Map<?,?> row)) throw new IllegalArgumentException("生命周期申请明细格式错误");
|
String versionKey=operation==Operation.REVISION?"yuan_wenjian_banben_id":"wenjian_banben_id";
|
String file=string(row.get("wenjian_id"));
|
String version=operation==Operation.EXTENSION?null:string(row.get(versionKey));
|
if(file==null || (operation!=Operation.EXTENSION && version==null)) {
|
if(status==0) continue; // never-submitted incomplete draft is checked by service ownership
|
throw new IllegalArgumentException("文件或来源版本缺失,请重新选择");
|
}
|
targets.add(new BusinessFileSubmissionService.Target(file,version));
|
}
|
submissions.submit(tenant,taskId,applicationId,taskId,operation,targets,status==0);
|
return true;
|
} catch(jnpf.dmsBusinessOccupancy.BusinessFileIneligibleException failure) {
|
String tenant=UserProvider.getUser().getTenantId();
|
tenant=tenant==null || tenant.isBlank() ? "0" : tenant;
|
throw new WorkFlowException(DmsLifecycleEligibilityMessages.render(mapper,tenant,failure));
|
} catch(Exception failure) {
|
throw new WorkFlowException(failure.getMessage());
|
}
|
}
|
@Override
|
public void validate(String formId, Map<String,Object> data) {
|
var form = mapper.publishedLifecycleForm(formId);
|
if (form == null || !"dms_wenjian_xiuding_shenhe".equals(form.getMainTable())) return;
|
try {
|
String key = detailKey(JSON.readTree(form.getFormData()).path("fields"), "dms_wenjian_xiuding_shenhe_jilu_mingxi");
|
if (key == null || !(data.get(key) instanceof List<?> records))
|
throw new IllegalArgumentException("修订审核必须提交完整记录明细");
|
String tenant = UserProvider.getUser().getTenantId();
|
validateRevisionAttachments(tenant == null || tenant.isBlank() ? "0" : tenant, data, records);
|
} catch (java.io.IOException failure) { throw new IllegalArgumentException("修订审核配置无效"); }
|
}
|
// Runs on canonical adopted attachments inside native save, after occupancy acquisition.
|
void validateRevisionAttachments(String tenant, Map<String,Object> data, List<?> records) {
|
if ("wubianhua".equals(string(data.get("biangeng_fangshi"))))
|
requireUnchangedAttachment(mapper.revisionMainAttachment(tenant, string(data.get("xiuding_shenqing_zixiang_id"))),
|
data.get("wenjian"), "主文件", tenant);
|
int index=0;
|
for(Object raw:records) {
|
index++;
|
if(!(raw instanceof Map<?,?> row)) throw new IllegalArgumentException("修订记录明细格式错误");
|
if("wubianhua".equals(string(row.get("biangeng_fangshi"))))
|
requireUnchangedAttachment(mapper.revisionAttachment(tenant,string(row.get("jilu_wenjian_id")),
|
string(row.get("yuan_wenjian_banben_id"))),row.get("jilu_wenjian"),"第"+index+"行记录", tenant);
|
}
|
}
|
private void requireUnchangedAttachment(String original,Object submitted,String label, String tenant) {
|
String cacheKey = comparisonKey(tenant, original, submitted);
|
Boolean equal = cacheKey == null ? null : contentChecks.get(cacheKey);
|
if (equal == null) {
|
equal = jnpf.file.FileContentEquality.same(original, submitted, fileContentApi);
|
if (cacheKey != null) contentChecks.put(cacheKey, equal);
|
}
|
if (equal) return;
|
throw new IllegalArgumentException(label+"选择了无变化,但附件与原版本不一致,请恢复原附件或将变更方式改为升级");
|
}
|
private static String comparisonKey(String tenant, String original, Object submitted) {
|
try {
|
JsonNode before = JSON.readTree(original);
|
JsonNode after = submitted instanceof String text ? JSON.readTree(text) : JSON.valueToTree(submitted);
|
if (!before.isArray() || before.isEmpty() || !after.isArray() || after.isEmpty() || before.size() != after.size()) return null;
|
List<Object> identity = new ArrayList<>(); identity.add("dms-original-byte-equality-v1"); identity.add(tenant);
|
for (JsonNode file : before) {
|
if (file.path("fileId").asText().isBlank() || file.path("url").asText().isBlank()) return null;
|
identity.add(Arrays.asList(file.path("fileId").asText(), file.path("url").asText()));
|
}
|
for (JsonNode file : after) {
|
if (!file.path("fileId").asText().matches("onlyoffice-edit-temp,[a-f0-9]{32},(?:review_)?[a-f0-9]{32},[^,]+")
|
|| file.path("url").asText().isBlank()) return null;
|
identity.add(Arrays.asList(file.path("fileId").asText(), file.path("url").asText()));
|
}
|
return cn.hutool.crypto.digest.DigestUtil.sha256Hex(JSON.writeValueAsString(identity));
|
} catch (Exception invalid) { return null; }
|
}
|
|
private static String string(Object value) {
|
return value instanceof String s && !s.isBlank() ? s : null;
|
}
|
private static String detailKey(JsonNode fields,String table) {
|
for(JsonNode field:fields) {
|
JsonNode config=field.path("__config__");
|
if("table".equals(config.path("jnpfKey").asText()) && table.equals(config.path("tableName").asText()))
|
return field.path("__vModel__").asText();
|
String child=detailKey(config.path("children"),table);if(child!=null)return child;
|
}
|
return null;
|
}
|
}
|