package jnpf.dmsService.impl;
|
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import jnpf.audit.sdk.annotation.AuditLog;
|
import org.springframework.transaction.annotation.Propagation;
|
import jnpf.base.Pagination;
|
import jnpf.base.UserInfo;
|
import jnpf.dmsEntity.DmsWenjianBianhaoEntity;
|
import jnpf.dmsEntity.DmsWenjianBianhaoGuizeEntity;
|
import jnpf.dmsEntity.DmsWenjianBianhaoGuizeZixiangEntity;
|
import jnpf.dmsEntity.DmsWenjianBianhaoShenqingEntity;
|
import jnpf.dmsEntity.DmsWenjianMingchengBiangengShenqingEntity;
|
import jnpf.dmsEntity.DmsWenjianZiyuanEntity;
|
import jnpf.dmsEntity.wenjianbianhao.DmsFlowEventType;
|
import jnpf.dmsEntity.wenjianbianhao.form.DmsWenjianBianhaoSubmitCheckForm;
|
import jnpf.dmsEntity.wenjianbianhao.vo.DmsWenjianBianhaoOptionVO;
|
import jnpf.dmsEntity.wenjianbianhao.vo.DmsWenjianBianhaoPreviewVO;
|
import jnpf.dmsEntity.wenjianbianhao.vo.DmsWenjianBianhaoRuleVO;
|
import jnpf.dmsMapper.DmsWenjianBianhaoGuizeMapper;
|
import jnpf.dmsMapper.DmsWenjianBianhaoGuizeZixiangMapper;
|
import jnpf.dmsMapper.DmsWenjianBianhaoLiushuiJishuMapper;
|
import jnpf.dmsMapper.DmsWenjianBianhaoMapper;
|
import jnpf.dmsMapper.DmsWenjianBianhaoShenqingMapper;
|
import jnpf.dmsMapper.DmsWenjianMingchengBiangengShenqingMapper;
|
import jnpf.dmsMapper.DmsWenjianZiyuanMapper;
|
import jnpf.dmsPermission.DmsCurrentIdentity;
|
import jnpf.dmsService.DmsWenjianBianhaoApplicationService;
|
import jnpf.dmsService.DmsWenjianBianhaoPermissionService;
|
import jnpf.dmsService.support.DmsWenjianBianhaoFlowStateResolver;
|
import jnpf.dmsService.support.DmsWenjianBianhaoFlowStateResolver.Action;
|
import jnpf.exception.DataException;
|
import jnpf.permission.UserApi;
|
import jnpf.permission.entity.UserEntity;
|
import lombok.RequiredArgsConstructor;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.Date;
|
import java.util.Comparator;
|
import java.util.IdentityHashMap;
|
import java.util.HashMap;
|
import java.util.HashSet;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Set;
|
import java.util.regex.Matcher;
|
import java.util.regex.Pattern;
|
|
@Slf4j
|
@Service
|
@RequiredArgsConstructor
|
public class DmsWenjianBianhaoApplicationServiceImpl implements DmsWenjianBianhaoApplicationService {
|
|
private static final String ENABLED = "enabled";
|
private static final int APPROVED_FLOW_STATE = 2;
|
private static final String DRAFT = "draft";
|
private static final String PENDING_REVIEW = "pending_review";
|
private static final String USING = "using";
|
private static final String REJECTED = "rejected";
|
private static final String CANCELLED = "cancelled";
|
private static final Pattern SERIAL_TOKEN = Pattern.compile("\\{SERIAL:(\\d+)\\}");
|
private static final ObjectMapper SNAPSHOT_MAPPER = new ObjectMapper()
|
.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
|
|
private final DmsCurrentIdentity identity;
|
private final DmsWenjianBianhaoPermissionService permissionService;
|
private final DmsWenjianBianhaoGuizeMapper ruleMapper;
|
private final DmsWenjianBianhaoGuizeZixiangMapper partMapper;
|
private final DmsWenjianBianhaoShenqingMapper applicationMapper;
|
private final DmsWenjianBianhaoMapper numberMapper;
|
private final DmsWenjianBianhaoLiushuiJishuMapper counterMapper;
|
private final DmsWenjianMingchengBiangengShenqingMapper nameChangeMapper;
|
private final DmsWenjianZiyuanMapper resourceMapper;
|
private final UserApi userApi;
|
|
@Override
|
public List<DmsWenjianBianhaoRuleVO> enabledRules(
|
Pagination pagination, String guizeId, String searchValue) throws DataException {
|
UserInfo user = identity.requireUser();
|
String tenantId = identity.tenantId(user);
|
List<DmsWenjianBianhaoRuleVO> result = new ArrayList<>();
|
List<DmsWenjianBianhaoGuizeEntity> rules = ruleMapper.selectEnabled(tenantId);
|
if (rules == null || rules.isEmpty()) {
|
pagination.setTotal(0);
|
return Collections.emptyList();
|
}
|
String resolvedRuleId = trim(guizeId);
|
String keyword = blank(searchValue) ? trim(pagination.getKeyword()) : trim(searchValue);
|
for (DmsWenjianBianhaoGuizeEntity rule : rules) {
|
if (!resolvedRuleId.isEmpty() && !resolvedRuleId.equals(rule.getId())) {
|
continue;
|
}
|
if (!keyword.isEmpty() && !matchesRule(rule, keyword)) {
|
continue;
|
}
|
if (!permissionService.canApplyRule(user.getUserId(), rule.getId())) {
|
continue;
|
}
|
RuleSnapshot snapshot;
|
try {
|
snapshot = loadSnapshot(tenantId, rule.getId());
|
} catch (DataException exception) {
|
// 候选列表跳过无效规则,实际申请仍由 loadSnapshot 严格校验。
|
log.warn("跳过不可申请的文件编号规则,tenantId={}, ruleId={}, reason={}",
|
tenantId, rule.getId(), exception.getMessage());
|
continue;
|
}
|
DmsWenjianBianhaoRuleVO vo = new DmsWenjianBianhaoRuleVO();
|
vo.setId(rule.getId());
|
vo.setGuizeMingcheng(rule.getGuizeMingcheng());
|
vo.setGuizeLeixing(rule.getGuizeLeixing());
|
vo.setChangqu(rule.getChangqu());
|
vo.setZidingyiTishi(blank(snapshot.customHint) ? "/" : snapshot.customHint);
|
vo.setWenjianMulu(rule.getWenjianMulu());
|
vo.setWenjianMuluId(rule.getWenjianMuluId());
|
vo.setYulanBianhao(snapshot.preview);
|
vo.setGuizeXianshi(display(rule.getGuizeMingcheng(), snapshot.preview));
|
result.add(vo);
|
}
|
pagination.setTotal(result.size());
|
long pageSize = Math.max(pagination.getPageSize(), 1L);
|
long currentPage = Math.max(pagination.getCurrentPage(), 1L);
|
if (currentPage - 1 > Long.MAX_VALUE / pageSize) {
|
return Collections.emptyList();
|
}
|
long offset = (currentPage - 1) * pageSize;
|
if (offset >= result.size()) {
|
return Collections.emptyList();
|
}
|
int fromIndex = (int) offset;
|
long available = result.size() - offset;
|
int toIndex = (int) (offset + Math.min(pageSize, available));
|
return new ArrayList<>(result.subList(fromIndex, toIndex));
|
}
|
|
private boolean matchesRule(DmsWenjianBianhaoGuizeEntity rule, String keyword) {
|
String normalized = keyword.toLowerCase();
|
return trim(rule.getGuizeMingcheng()).toLowerCase().contains(normalized)
|
|| trim(rule.getGuizeLeixing()).toLowerCase().contains(normalized)
|
|| trim(rule.getWenjianMulu()).toLowerCase().contains(normalized);
|
}
|
|
@Override
|
public DmsWenjianBianhaoPreviewVO preview(String guizeId, String zidingyiNeirong) throws DataException {
|
UserInfo user = identity.requireUser();
|
requirePermission(user.getUserId(), guizeId);
|
RuleSnapshot snapshot = loadSnapshot(identity.tenantId(user), guizeId, zidingyiNeirong, false);
|
return new DmsWenjianBianhaoPreviewVO(guizeId, snapshot.preview, snapshot.hasSerial, snapshot.hasCustom, snapshot.customHint);
|
}
|
|
@Override
|
public List<DmsWenjianBianhaoOptionVO> usingNumbers(
|
Pagination pagination, String bianhaoId, String searchValue) throws DataException {
|
UserInfo user = identity.requireUser();
|
String tenantId = identity.tenantId(user);
|
if (blank(pagination.getKeyword()) && !blank(searchValue)) {
|
pagination.setKeyword(trim(searchValue));
|
}
|
List<DmsWenjianBianhaoEntity> numbers = numberMapper.selectUsing(
|
tenantId, trim(bianhaoId), pagination);
|
if (numbers == null || numbers.isEmpty()) {
|
return Collections.emptyList();
|
}
|
return toNumberOptions(user, numbers);
|
}
|
|
@Override
|
public List<DmsWenjianBianhaoOptionVO> availableNameChangeNumbers(
|
Pagination pagination, String bianhaoId, String searchValue) throws DataException {
|
UserInfo user = identity.requireUser();
|
String tenantId = identity.tenantId(user);
|
if (blank(pagination.getKeyword()) && !blank(searchValue)) {
|
pagination.setKeyword(trim(searchValue));
|
}
|
List<DmsWenjianBianhaoEntity> numbers = numberMapper.selectNameChangeAvailable(
|
tenantId, trim(bianhaoId), pagination);
|
if (numbers == null || numbers.isEmpty()) {
|
return Collections.emptyList();
|
}
|
return toNumberOptions(user, numbers, true);
|
}
|
|
@Override
|
public List<DmsWenjianBianhaoOptionVO> availableNewNumbers(
|
Pagination pagination, String bianhaoId, String searchValue) throws DataException {
|
UserInfo user = identity.requireUser();
|
String tenantId = identity.tenantId(user);
|
if (blank(pagination.getKeyword()) && !blank(searchValue)) {
|
pagination.setKeyword(trim(searchValue));
|
}
|
List<DmsWenjianBianhaoEntity> numbers = numberMapper.selectAvailableNew(
|
tenantId, trim(bianhaoId), pagination);
|
if (numbers == null || numbers.isEmpty()) {
|
return Collections.emptyList();
|
}
|
return toNumberOptions(user, numbers);
|
}
|
|
private List<DmsWenjianBianhaoOptionVO> toNumberOptions(
|
UserInfo user, List<DmsWenjianBianhaoEntity> numbers) {
|
return toNumberOptions(user, numbers, false);
|
}
|
|
private List<DmsWenjianBianhaoOptionVO> toNumberOptions(
|
UserInfo user, List<DmsWenjianBianhaoEntity> numbers, boolean includeSite) {
|
List<DmsWenjianBianhaoEntity> allowedNumbers = new ArrayList<>(numbers.size());
|
for (DmsWenjianBianhaoEntity number : numbers) {
|
if (permissionService.canViewNumber(user.getUserId(), number.getId())) {
|
allowedNumbers.add(number);
|
}
|
}
|
Map<String, String> userNames = loadUserNames(allowedNumbers);
|
List<DmsWenjianBianhaoOptionVO> result = new ArrayList<>(allowedNumbers.size());
|
for (DmsWenjianBianhaoEntity number : allowedNumbers) {
|
DmsWenjianBianhaoOptionVO vo = new DmsWenjianBianhaoOptionVO();
|
vo.setId(number.getId());
|
vo.setWenjianBianhao(number.getWenjianBianhao());
|
vo.setWenjianMingcheng(number.getDangqianWenjianMingcheng());
|
vo.setMingchengBanben(number.getMingchengBanben());
|
vo.setGuizeMingcheng(number.getGuizeMingchengKuaizhao());
|
if (includeSite) {
|
vo.setChangqu(numberSnapshotSite(number.getGuizeKuaizhao()));
|
}
|
vo.setShiyongRenId(number.getShiyongRenId());
|
vo.setShiyongRen(userNames.get(number.getShiyongRenId()));
|
vo.setBianhaoFafangShijian(number.getBianhaoFafangShijian());
|
vo.setBianhaoXianshi(display(number.getWenjianBianhao(),
|
number.getDangqianWenjianMingcheng()));
|
result.add(vo);
|
}
|
return result;
|
}
|
|
private String numberSnapshotSite(String source) {
|
if (blank(source)) {
|
return null;
|
}
|
try {
|
JsonNode root = SNAPSHOT_MAPPER.readTree(source);
|
if (root == null || !root.isObject()) {
|
return null;
|
}
|
// 顶层字段存在时保留其空值语义,仅旧快照回退到 rule.changqu。
|
JsonNode site = root.has("changqu") ? root.path("changqu") : root.path("rule").path("changqu");
|
return site.isTextual() ? site.textValue() : null;
|
} catch (JsonProcessingException ignored) {
|
return null;
|
}
|
}
|
|
private Map<String, String> loadUserNames(List<DmsWenjianBianhaoEntity> numbers) {
|
List<String> userIds = new ArrayList<>();
|
for (DmsWenjianBianhaoEntity number : numbers) {
|
String userId = trim(number.getShiyongRenId());
|
if (!userId.isEmpty() && !userIds.contains(userId)) {
|
userIds.add(userId);
|
}
|
}
|
if (userIds.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
List<UserEntity> users = userApi.getUserName(userIds);
|
if (users == null || users.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
Map<String, String> result = new HashMap<>();
|
for (UserEntity user : users) {
|
if (user != null && !blank(user.getId())) {
|
result.put(user.getId(), user.getRealName());
|
}
|
}
|
return result;
|
}
|
|
@Override
|
@Transactional(readOnly = true)
|
public void validateSubmission(DmsWenjianBianhaoSubmitCheckForm form) throws DataException {
|
UserInfo user = identity.requireUser();
|
String tenantId = identity.tenantId(user);
|
if (form == null || form.getItems() == null || form.getItems().isEmpty()) {
|
throw new DataException("文件编号申请至少需要一条编号明细");
|
}
|
String applicationId = trim(form.getApplicationId());
|
Map<String, DmsWenjianBianhaoEntity> persisted = submissionRows(tenantId, user.getUserId(), form);
|
DmsWenjianBianhaoShenqingEntity application = applicationId.isEmpty() ? null
|
: applicationMapper.selectActiveById(tenantId, applicationId);
|
boolean useFrozenSnapshot = application != null
|
&& !Set.of(0, 9).contains(application.getFlowState())
|
&& !(Integer.valueOf(8).equals(application.getFlowState())
|
&& Objects.equals(user.getUserId(), application.getShenqingRenId()));
|
Map<List<String>, RuleSnapshot> rules = new HashMap<>();
|
Map<String, Integer> fixedNumbers = new LinkedHashMap<>();
|
Set<String> rowIds = new HashSet<>();
|
for (int i = 0; i < form.getItems().size(); i++) {
|
DmsWenjianBianhaoSubmitCheckForm.Item item = form.getItems().get(i);
|
if (item == null) throw new DataException("编号申请明细不能为空");
|
String ruleId = requireText(item.getGuizeId(), "第" + (i + 1) + "行未选择编号规则");
|
String rowId = trim(item.getId());
|
if (!rowId.isEmpty() && !rowIds.add(rowId)) {
|
throw new DataException("编号申请明细重复,请删除重复行后提交");
|
}
|
DmsWenjianBianhaoEntity stored = persisted.get(rowId);
|
if (stored != null && !blank(stored.getWenjianBianhao())) {
|
throw new DataException("已发放的文件编号不能重新提交:" + stored.getWenjianBianhao());
|
}
|
String template;
|
boolean hasSerial;
|
if (useFrozenSnapshot && stored != null && Objects.equals(ruleId, stored.getGuizeId()) && hasReusableSnapshot(stored)
|
&& Objects.equals(trim(stored.getZidingyiNeirong()), trim(item.getZidingyiNeirong()))) {
|
// Approval also invokes beforeSubmit; use only the persisted snapshot, never client hidden fields.
|
template = stored.getBianhaoMuban();
|
hasSerial = SERIAL_TOKEN.matcher(template).find();
|
} else {
|
List<String> key = List.of(ruleId, trim(item.getZidingyiNeirong()));
|
RuleSnapshot snapshot = rules.get(key);
|
if (snapshot == null) {
|
requirePermission(user.getUserId(), ruleId);
|
snapshot = loadSnapshot(tenantId, ruleId, item.getZidingyiNeirong(), true);
|
rules.put(key, snapshot);
|
}
|
template = snapshot.template;
|
hasSerial = snapshot.hasSerial;
|
}
|
if (!hasSerial) {
|
Integer previous = fixedNumbers.putIfAbsent(template, i + 1);
|
if (previous != null) {
|
throw new DataException("第" + previous + "行与第" + (i + 1) + "行生成相同编号【"
|
+ template + "】,无流水号的编号只能申请一次,请删除重复明细");
|
}
|
}
|
}
|
if (fixedNumbers.isEmpty()) return;
|
List<DmsWenjianBianhaoEntity> conflicts = numberMapper.selectFixedNumberConflicts(
|
tenantId, new ArrayList<>(fixedNumbers.keySet()), applicationId.isEmpty() ? null : applicationId);
|
if (!conflicts.isEmpty()) {
|
DmsWenjianBianhaoEntity conflict = conflicts.get(0);
|
boolean issued = !blank(conflict.getWenjianBianhao());
|
String number = issued ? conflict.getWenjianBianhao() : conflict.getBianhaoMuban();
|
throw new DataException("编号【" + number + "】"
|
+ (issued ? "已发放,不能重复申请" : "已被其他审批中的申请占用,不能重复申请"));
|
}
|
}
|
|
private Map<String, DmsWenjianBianhaoEntity> submissionRows(
|
String tenantId, String userId, DmsWenjianBianhaoSubmitCheckForm form) throws DataException {
|
Map<String, DmsWenjianBianhaoEntity> rows = new HashMap<>();
|
String applicationId = trim(form.getApplicationId());
|
if (applicationId.isEmpty()) return rows;
|
DmsWenjianBianhaoShenqingEntity application = applicationMapper.selectActiveById(tenantId, applicationId);
|
if (application == null || application.getFlowState() == null
|
|| !Set.of(0, 1, 5, 6, 8, 9, 10).contains(application.getFlowState())) {
|
throw new DataException("编号申请不存在或已结束,请刷新后重试");
|
}
|
boolean applicant = Objects.equals(userId, application.getShenqingRenId());
|
boolean draft = Set.of(0, 9).contains(application.getFlowState());
|
// A returned task can be at an approval node rather than at its applicant.
|
boolean editable = draft || (application.getFlowState() == 8 && applicant);
|
if (draft && !applicant) {
|
throw new DataException("只能提交本人未结束的编号申请");
|
}
|
for (DmsWenjianBianhaoEntity row : numberMapper.selectByApplication(tenantId, applicationId)) {
|
rows.put(row.getId(), row);
|
}
|
if (!editable) {
|
Set<String> selected = new HashSet<>();
|
for (DmsWenjianBianhaoSubmitCheckForm.Item item : form.getItems()) {
|
DmsWenjianBianhaoEntity row = item == null ? null : rows.get(trim(item.getId()));
|
if (row == null || !Objects.equals(row.getGuizeId(), trim(item.getGuizeId()))
|
|| !Objects.equals(trim(row.getZidingyiNeirong()), trim(item.getZidingyiNeirong()))
|
|| !selected.add(row.getId())) {
|
throw new DataException("审批中的编号明细与原申请不一致,不能变更编号规则或自定义内容");
|
}
|
}
|
if (selected.size() != rows.size()) {
|
throw new DataException("审批中的编号明细与原申请不一致,不能增删明细");
|
}
|
}
|
return rows;
|
}
|
|
private String display(String first, String second) {
|
String left = trim(first);
|
String right = trim(second);
|
if (left.isEmpty()) {
|
return right;
|
}
|
return right.isEmpty() ? left : left + " / " + right;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void syncApplicationFlow(String flowId, String taskId, Integer flowState,
|
DmsFlowEventType eventType) throws DataException {
|
UserInfo currentUser = identity.requireUser();
|
String tenantId = identity.tenantId(currentUser);
|
String resolvedFlowId = requireText(flowId, "流程ID不能为空");
|
String resolvedTaskId = requireText(taskId, "流程任务ID不能为空");
|
Action action = resolveAction(flowState);
|
|
DmsWenjianBianhaoShenqingEntity application = findApplicationForUpdate(
|
tenantId, resolvedFlowId, resolvedTaskId);
|
verifyPersistedFlowState(application.getFlowState(), flowState, eventType);
|
List<DmsWenjianBianhaoEntity> numbers = numberMapper.selectByApplicationForUpdate(
|
tenantId, application.getId());
|
if (numbers == null || numbers.isEmpty()) {
|
throw new DataException("文件编号申请至少需要一条编号明细");
|
}
|
|
switch (action) {
|
case RESTORE_DRAFT:
|
changeUnissuedStatus(numbers, DRAFT);
|
break;
|
case FREEZE_SNAPSHOT:
|
freezeSnapshots(application, numbers);
|
break;
|
case ISSUE_NUMBER:
|
issueNumbers(tenantId, numbers);
|
break;
|
case REJECT:
|
changeUnissuedStatus(numbers, REJECTED);
|
break;
|
case CANCEL:
|
changeUnissuedStatus(numbers, CANCELLED);
|
break;
|
case REVOKE:
|
revokeUnissuedNumbers(numbers);
|
break;
|
case HOLD:
|
break;
|
case ERROR:
|
throw new DataException("文件编号申请流程处于异常状态,请修复流程后重试");
|
default:
|
throw new DataException("不支持的文件编号申请流程动作:" + action);
|
}
|
log.info("文件编号申请流程事件同步完成,flowId={}, taskId={}, event={}, flowState={}, action={}",
|
resolvedFlowId, resolvedTaskId, eventType, flowState, action);
|
}
|
|
@Override
|
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
|
@AuditLog(action = "DMS_NUMBER_ISSUE_FAILED", label = "回写发号失败", bizModule = "DMS",
|
bizType = "DMS_NUMBER_APPLICATION", bizCodeExpr = "#p1", bizCodeRequired = false,
|
targetTable = "dms_wenjian_bianhao_shenqing", targetIdExpr = "#p1")
|
public void markIssuanceFailed(String flowId, String taskId) throws DataException {
|
String tenantId = identity.tenantId(identity.requireUser());
|
DmsWenjianBianhaoShenqingEntity application = findApplicationForUpdate(tenantId,
|
requireText(flowId, "流程ID不能为空"), requireText(taskId, "流程任务ID不能为空"));
|
verifyPersistedFlowState(application.getFlowState(), APPROVED_FLOW_STATE, DmsFlowEventType.END);
|
// Lock in the same order as issuance. Never overwrite a concurrently completed number.
|
numberMapper.selectByApplicationForUpdate(tenantId, application.getId());
|
numberMapper.markUnissuedFailure(tenantId, application.getId());
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void syncNameChangeFlow(String flowId, String taskId, Integer flowState,
|
DmsFlowEventType eventType) throws DataException {
|
UserInfo currentUser = identity.requireUser();
|
String tenantId = identity.tenantId(currentUser);
|
String resolvedFlowId = requireText(flowId, "流程ID不能为空");
|
String resolvedTaskId = requireText(taskId, "流程任务ID不能为空");
|
Action action = resolveAction(flowState);
|
|
DmsWenjianMingchengBiangengShenqingEntity change = findNameChangeForUpdate(
|
tenantId, resolvedFlowId, resolvedTaskId);
|
verifyPersistedFlowState(change.getFlowState(), flowState, eventType);
|
|
if (action == Action.FREEZE_SNAPSHOT) {
|
freezeNameChange(tenantId, change);
|
} else if (action == Action.ISSUE_NUMBER) {
|
completeNameChange(tenantId, change);
|
} else if (action == Action.ERROR) {
|
throw new DataException("文件名称变更流程处于异常状态,请修复流程后重试");
|
}
|
log.info("文件名称变更流程事件同步完成,flowId={}, taskId={}, event={}, flowState={}, action={}",
|
resolvedFlowId, resolvedTaskId, eventType, flowState, action);
|
}
|
|
private DmsWenjianBianhaoShenqingEntity findApplicationForUpdate(
|
String tenantId, String flowId, String taskId) throws DataException {
|
List<DmsWenjianBianhaoShenqingEntity> applications =
|
applicationMapper.selectByFlowForUpdate(tenantId, flowId, taskId);
|
if (applications == null || applications.isEmpty()) {
|
throw new DataException("未找到文件编号申请流程关联记录");
|
}
|
if (applications.size() > 1) {
|
throw new DataException("多个文件编号申请关联同一流程任务");
|
}
|
return applications.get(0);
|
}
|
|
private DmsWenjianMingchengBiangengShenqingEntity findNameChangeForUpdate(
|
String tenantId, String flowId, String taskId) throws DataException {
|
List<DmsWenjianMingchengBiangengShenqingEntity> changes =
|
nameChangeMapper.selectByFlowForUpdate(tenantId, flowId, taskId);
|
if (changes == null || changes.isEmpty()) {
|
throw new DataException("未找到文件名称变更流程关联记录");
|
}
|
if (changes.size() > 1) {
|
throw new DataException("多个文件名称变更申请关联同一流程任务");
|
}
|
return changes.get(0);
|
}
|
|
private void verifyPersistedFlowState(Integer persistedState, Integer workflowState,
|
DmsFlowEventType eventType) throws DataException {
|
if (eventType == null) {
|
throw new DataException("流程事件类型不能为空");
|
}
|
if (!Objects.equals(persistedState, workflowState)) {
|
throw new DataException("流程状态尚未同步完成:表单状态=" + persistedState
|
+ ",任务状态=" + workflowState + ",请稍后重试");
|
}
|
}
|
|
private Action resolveAction(Integer flowState) throws DataException {
|
return DmsWenjianBianhaoFlowStateResolver.resolve(flowState)
|
.orElseThrow(() -> new DataException("未知的工作流状态:" + flowState));
|
}
|
|
private void freezeSnapshots(DmsWenjianBianhaoShenqingEntity application,
|
List<DmsWenjianBianhaoEntity> numbers) throws DataException {
|
String applicantId = requireText(application.getShenqingRenId(), "编号申请人不能为空");
|
for (DmsWenjianBianhaoEntity number : numbers) {
|
if (!blank(number.getWenjianBianhao())) {
|
throw new DataException("已发放的文件编号不能重新提交:" + number.getWenjianBianhao());
|
}
|
String originalName = requireText(number.getYuanWenjianMingcheng(), "文件名称不能为空");
|
requirePermission(applicantId, number.getGuizeId());
|
RuleSnapshot snapshot = loadSnapshot(application.getTenantId(), number.getGuizeId(),
|
number.getZidingyiNeirong(), true);
|
|
number.setGuizeKuaizhaoId(number.getGuizeId());
|
number.setGuizeMingchengKuaizhao(snapshot.ruleName);
|
// 目录已在填写编号申请时保存,提交不能用规则的新目录覆盖。
|
String folderId = requireText(number.getWenjianMuluId(), "编号申请缺少目录,请重新选择目录");
|
ObjectNode frozen = snapshotFields(snapshot.json);
|
frozen.put("wenjian_mulu_id", folderId);
|
number.setGuizeKuaizhao(frozen.toString());
|
number.setBianhaoMuban(snapshot.template);
|
number.setYulanBianhao(snapshot.preview);
|
number.setShenqingBianhao(snapshot.preview);
|
if (blank(number.getShiyongRenId())) {
|
number.setShiyongRenId(applicantId);
|
}
|
number.setDangqianWenjianMingcheng(originalName);
|
number.setMingchengBanben(1);
|
number.setBizStatus(PENDING_REVIEW);
|
requireSingleUpdate(numberMapper.updateById(number), "冻结文件编号规则快照失败");
|
}
|
}
|
|
private boolean hasReusableSnapshot(DmsWenjianBianhaoEntity number) throws DataException {
|
return PENDING_REVIEW.equals(number.getBizStatus())
|
&& !blank(number.getWenjianMuluId())
|
&& number.getGuizeKuaizhao() != null
|
&& number.getGuizeKuaizhao().contains("\"wenjian_mulu_id\"")
|
&& !blank(number.getGuizeKuaizhao())
|
&& !blank(number.getBianhaoMuban())
|
&& Objects.equals(number.getGuizeKuaizhaoId(), number.getGuizeId())
|
&& customSnapshotMatches(number);
|
}
|
|
private void issueNumbers(String tenantId,
|
List<DmsWenjianBianhaoEntity> numbers) throws DataException {
|
// 一张申请可能使用多个流水组;统一获取顺序,避免并发申请反向持锁。
|
Map<DmsWenjianBianhaoEntity, SequenceGroup> groups = new IdentityHashMap<>();
|
List<DmsWenjianBianhaoEntity> unissued = new ArrayList<>();
|
for (DmsWenjianBianhaoEntity number : numbers) {
|
if (!blank(number.getWenjianBianhao())) continue;
|
if (!PENDING_REVIEW.equals(number.getBizStatus())
|
|| blank(number.getGuizeKuaizhao()) || blank(number.getBianhaoMuban())
|
|| blank(number.getGuizeKuaizhaoId())) {
|
throw new DataException("文件编号缺少发起时冻结的规则快照,不能发号:" + number.getId());
|
}
|
groups.put(number, SERIAL_TOKEN.matcher(number.getBianhaoMuban()).find()
|
? sequenceGroup(tenantId, number) : new SequenceGroup("", ""));
|
unissued.add(number);
|
}
|
// List.sort 为稳定排序,同组仍按明细原顺序发号。
|
unissued.sort(Comparator.comparing((DmsWenjianBianhaoEntity n) -> groups.get(n).family)
|
.thenComparing(n -> groups.get(n).key));
|
Map<DmsWenjianBianhaoEntity, AssignedNumber> assignedNumbers = new IdentityHashMap<>();
|
for (DmsWenjianBianhaoEntity number : unissued) {
|
assignedNumbers.put(number, renderFinalNumber(tenantId, number, groups.get(number)));
|
}
|
for (DmsWenjianBianhaoEntity number : numbers) {
|
if (!blank(number.getWenjianBianhao())) {
|
boolean changed = false;
|
if (!Objects.equals(number.getShenqingBianhao(), number.getWenjianBianhao())) {
|
number.setShenqingBianhao(number.getWenjianBianhao());
|
changed = true;
|
}
|
if (!USING.equals(number.getBizStatus())) {
|
number.setBizStatus(USING);
|
changed = true;
|
}
|
if (changed) {
|
requireSingleUpdate(numberMapper.updateById(number), "恢复已发放文件编号状态失败");
|
}
|
continue;
|
}
|
if (!PENDING_REVIEW.equals(number.getBizStatus())
|
|| blank(number.getGuizeKuaizhao())
|
|| blank(number.getBianhaoMuban())
|
|| blank(number.getGuizeKuaizhaoId())) {
|
throw new DataException("文件编号缺少发起时冻结的规则快照,不能发号:" + number.getId());
|
}
|
|
AssignedNumber assigned = assignedNumbers.get(number);
|
number.setWenjianBianhao(assigned.number);
|
number.setShenqingBianhao(assigned.number);
|
number.setLiushuiHao(assigned.serial);
|
number.setBianhaoFafangShijian(new Date());
|
number.setBizStatus(USING);
|
requireSingleUpdate(numberMapper.updateById(number), "写入正式文件编号失败");
|
}
|
}
|
|
private AssignedNumber renderFinalNumber(String tenantId,
|
DmsWenjianBianhaoEntity number, SequenceGroup group) throws DataException {
|
String template = number.getBianhaoMuban();
|
Matcher matcher = SERIAL_TOKEN.matcher(template);
|
if (!matcher.find()) {
|
return new AssignedNumber(template, null);
|
}
|
int start = matcher.start();
|
int end = matcher.end();
|
int width = parseSerialWidth(matcher.group(1));
|
if (matcher.find()) {
|
throw new DataException("一个编号规则只能配置一个流水号组成项");
|
}
|
Long value = counterMapper.next(tenantId, group.family, group.key,
|
frozenSerialStart(number.getGuizeKuaizhao()));
|
if (value == null) {
|
throw new DataException("流水号分配失败");
|
}
|
String serial = leftPad(value, width);
|
return new AssignedNumber(template.substring(0, start) + serial + template.substring(end), serial);
|
}
|
|
private SequenceGroup sequenceGroup(String tenantId, DmsWenjianBianhaoEntity number)
|
throws DataException {
|
ObjectNode snapshot = snapshotFields(number.getGuizeKuaizhao());
|
String family = trim(snapshot.path("guize_liushuihao").asText(""));
|
String nested = trim(snapshot.path("rule").path("guize_liushuihao").asText(""));
|
if (!family.isEmpty() && !nested.isEmpty() && !family.equals(nested)) {
|
throw new DataException("冻结规则流水号不一致,不能发号:" + number.getId());
|
}
|
if (family.isEmpty()) family = nested;
|
if (family.isEmpty()) {
|
// 仅兼容缺失标识的旧快照,包含停用/软删除版本,绝不查询最新版本。
|
DmsWenjianBianhaoGuizeEntity historical = ruleMapper.selectHistoricalById(
|
tenantId, number.getGuizeKuaizhaoId());
|
family = historical == null ? "" : trim(historical.getGuizeLiushuihao());
|
}
|
if (family.isEmpty()) {
|
throw new DataException("冻结规则缺少规则流水号,需核实历史归属后发号:" + number.getId());
|
}
|
// 自定义内容已在发起时代入模板;组成方式和版本不改变流水归属。
|
return new SequenceGroup(family, "template:" + number.getBianhaoMuban());
|
}
|
|
private static final class SequenceGroup {
|
private final String family;
|
private final String key;
|
|
private SequenceGroup(String family, String key) {
|
this.family = family;
|
this.key = key;
|
}
|
}
|
|
private void changeUnissuedStatus(List<DmsWenjianBianhaoEntity> numbers,
|
String targetStatus) throws DataException {
|
for (DmsWenjianBianhaoEntity number : numbers) {
|
if (!blank(number.getWenjianBianhao()) || USING.equals(number.getBizStatus())) {
|
continue;
|
}
|
if (targetStatus.equals(number.getBizStatus())) {
|
continue;
|
}
|
number.setBizStatus(targetStatus);
|
requireSingleUpdate(numberMapper.updateById(number), "更新文件编号状态失败");
|
}
|
}
|
|
private void revokeUnissuedNumbers(List<DmsWenjianBianhaoEntity> numbers) throws DataException {
|
for (DmsWenjianBianhaoEntity number : numbers) {
|
if (!blank(number.getWenjianBianhao()) || USING.equals(number.getBizStatus())) {
|
log.warn("已发放编号关联流程被撤销,保留编号并等待人工处理,numberId={}, number={}",
|
number.getId(), number.getWenjianBianhao());
|
continue;
|
}
|
if (!CANCELLED.equals(number.getBizStatus())) {
|
number.setBizStatus(CANCELLED);
|
requireSingleUpdate(numberMapper.updateById(number), "撤销文件编号失败");
|
}
|
}
|
}
|
|
private void freezeNameChange(String tenantId,
|
DmsWenjianMingchengBiangengShenqingEntity change) throws DataException {
|
String numberId = requireText(change.getBianhaoId(), "文件编号ID不能为空");
|
requireText(change.getBiangengHouWenjianMingcheng(), "变更后文件名称不能为空");
|
DmsWenjianBianhaoEntity number = numberMapper.selectActiveByIdForUpdate(tenantId, numberId);
|
assertNameChangeAllowed(tenantId, numberId);
|
if (number == null || !USING.equals(number.getBizStatus()) || blank(number.getWenjianBianhao())) {
|
throw new DataException("文件编号不存在或尚未发放");
|
}
|
if (nameChangeMapper.countActiveByNumber(tenantId, numberId, change.getId()) > 0) {
|
throw new DataException("该文件编号已有进行中的名称变更申请,请勿重复提交");
|
}
|
change.setYuanWenjianMingcheng(number.getDangqianWenjianMingcheng());
|
change.setYuanMingchengBanben(currentNameVersion(number));
|
requireSingleUpdate(nameChangeMapper.updateById(change), "冻结文件名称版本失败");
|
}
|
|
private void completeNameChange(String tenantId,
|
DmsWenjianMingchengBiangengShenqingEntity change) throws DataException {
|
String numberId = requireText(change.getBianhaoId(), "文件编号ID不能为空");
|
String newName = requireText(change.getBiangengHouWenjianMingcheng(), "变更后文件名称不能为空");
|
if (change.getYuanMingchengBanben() == null) {
|
throw new DataException("名称变更申请缺少发起时冻结的名称版本");
|
}
|
|
DmsWenjianBianhaoEntity number = numberMapper.selectActiveByIdForUpdate(tenantId, numberId);
|
assertNameChangeAllowed(tenantId, numberId);
|
if (number == null || !USING.equals(number.getBizStatus())) {
|
throw new DataException("文件编号不存在或尚未发放");
|
}
|
int currentVersion = currentNameVersion(number);
|
int expectedVersion = change.getYuanMingchengBanben();
|
if (currentVersion == expectedVersion + 1
|
&& Objects.equals(number.getDangqianWenjianMingcheng(), newName)) {
|
return;
|
}
|
if (currentVersion != expectedVersion) {
|
throw new DataException("文件名称已被其他变更流程更新,请重新发起");
|
}
|
number.setDangqianWenjianMingcheng(newName);
|
number.setMingchengBanben(currentVersion + 1);
|
requireSingleUpdate(numberMapper.updateById(number), "更新文件名称失败");
|
}
|
|
private void assertNameChangeAllowed(String tenantId, String numberId) throws DataException {
|
if (numberMapper.numberAlreadyUsed(tenantId, numberId)) {
|
throw new DataException("文件编号已被使用(包括审核草稿),不能变更文件名称");
|
}
|
}
|
|
private int currentNameVersion(DmsWenjianBianhaoEntity number) {
|
return number.getMingchengBanben() == null ? 1 : number.getMingchengBanben();
|
}
|
|
private boolean customSnapshotMatches(DmsWenjianBianhaoEntity number) throws DataException {
|
if (blank(number.getGuizeKuaizhao())) return true;
|
ObjectNode snapshot = snapshotFields(number.getGuizeKuaizhao());
|
return !snapshot.path("hasCustom").asBoolean(false)
|
|| Objects.equals(snapshot.path("customValue").asText(""), trim(number.getZidingyiNeirong()));
|
}
|
|
private RuleSnapshot loadSnapshot(String tenantId, String ruleId) throws DataException {
|
return loadSnapshot(tenantId, ruleId, null, false);
|
}
|
|
private RuleSnapshot loadSnapshot(String tenantId, String ruleId, String customValue,
|
boolean requireCustom) throws DataException {
|
String resolvedRuleId = requireText(ruleId, "编号规则ID不能为空");
|
String resolvedTenantId = requireText(tenantId, "租户ID不能为空");
|
DmsWenjianBianhaoGuizeEntity rule = ruleMapper.selectActiveById(
|
resolvedTenantId, resolvedRuleId);
|
if (rule == null || !ENABLED.equals(rule.getBizEnabled())
|
|| !Objects.equals(APPROVED_FLOW_STATE, rule.getFlowState())) {
|
throw new DataException("文件编号规则不存在或不可用,请重新选择已启用且审批通过的规则");
|
}
|
List<DmsWenjianBianhaoGuizeZixiangEntity> parts = partMapper.selectActiveByRule(
|
resolvedTenantId, resolvedRuleId);
|
if (parts == null || parts.isEmpty()) {
|
throw new DataException("文件编号规则未配置明细");
|
}
|
|
StringBuilder template = new StringBuilder();
|
ObjectNode rawRule = snapshotFields(rule.getSnapshotData());
|
ObjectNode json = SNAPSHOT_MAPPER.createObjectNode();
|
// 原始行完整保留;兼容键继续供已有申请/审核逻辑读取。
|
json.set("rule", rawRule);
|
json.put("ruleId", resolvedRuleId);
|
json.put("ruleName", trim(rule.getGuizeMingcheng()));
|
json.put("ruleType", trim(rule.getGuizeLeixing()));
|
json.put("banbenhao", rule.getBanbenhao());
|
json.set("guize_liushuihao", rawRule.get("guize_liushuihao"));
|
json.put("changqu", rule.getChangqu());
|
json.put("qianpiye_id", rule.getQianpiyeId());
|
json.put("qianpiye_files", rule.getQianpiyeFiles());
|
ArrayNode snapshotParts = json.putArray("parts");
|
boolean hasSerial = false;
|
boolean hasCustom = false;
|
String customHint = "";
|
String serialPreview = "";
|
for (int i = 0; i < parts.size(); i++) {
|
DmsWenjianBianhaoGuizeZixiangEntity part = parts.get(i);
|
String type = trim(part.getBianhaoZuheLeixing());
|
String value = trim(part.getZhi());
|
String separator = trim(part.getFengefu());
|
if (isSerialType(type)) {
|
if (hasSerial) {
|
throw new DataException("一个编号规则只能配置一个流水号组成项");
|
}
|
long serialStart = parseSerialStart(value);
|
serialPreview = value;
|
int width = value.length();
|
json.put("serialStart", serialStart);
|
template.append("{SERIAL:").append(width).append('}');
|
hasSerial = true;
|
} else if ("zidingyi".equalsIgnoreCase(type)) {
|
if (hasCustom) throw new DataException("一个编号规则只能配置一个自定义组成项");
|
hasCustom = true;
|
customHint = value;
|
value = trim(customValue);
|
if (requireCustom && value.isEmpty()) throw new DataException("请填写自定义编号内容");
|
if (value.contains("{SERIAL:")) throw new DataException("自定义编号内容不能包含流水号模板标记");
|
if (value.length() > 255) throw new DataException("自定义编号内容不能超过255个字符");
|
json.put("customValue", value);
|
template.append(value.isEmpty() ? "待填写" : value);
|
} else {
|
template.append(value);
|
}
|
template.append(separator);
|
ObjectNode snapshotPart = snapshotParts.addObject();
|
snapshotPart.put("order", i + 1);
|
snapshotPart.put("id", trim(part.getId()));
|
snapshotPart.put("type", type);
|
snapshotPart.put("value", value);
|
snapshotPart.put("separator", separator);
|
snapshotPart.set("fields", snapshotFields(part.getSnapshotData()));
|
}
|
json.put("hasCustom", hasCustom);
|
String preview = SERIAL_TOKEN.matcher(template).replaceAll(serialPreview);
|
if (hasCustom && template.length() > 255) throw new DataException("完整编号模板不能超过255个字符");
|
return new RuleSnapshot(rule.getGuizeMingcheng(), template.toString(), preview,
|
hasSerial, hasCustom, customHint, json.toString());
|
}
|
|
private ObjectNode snapshotFields(String source) throws DataException {
|
if (!blank(source)) {
|
try {
|
JsonNode fields = SNAPSHOT_MAPPER.readTree(source);
|
if (fields instanceof ObjectNode) {
|
return (ObjectNode) fields;
|
}
|
} catch (JsonProcessingException ignored) {
|
// 不输出原始字段,避免把附件或签名内容写入异常日志。
|
}
|
}
|
throw new DataException("文件编号规则完整快照读取失败,请重新选择规则后提交");
|
}
|
|
private void requirePermission(String userId, String ruleId) throws DataException {
|
String resolvedRuleId = requireText(ruleId, "编号规则ID不能为空");
|
if (!permissionService.canApplyRule(userId, resolvedRuleId)) {
|
throw new DataException("无权申请该文件编号规则");
|
}
|
}
|
|
private long parseSerialStart(String value) throws DataException {
|
if (blank(value) || !value.matches("[0-9]+")) {
|
throw new DataException("流水号模板必须由数字组成,位数决定补零长度,非零值为起始号");
|
}
|
try {
|
return Math.max(1L, Long.parseLong(value));
|
} catch (NumberFormatException exception) {
|
throw new DataException("流水号起始值超出支持范围");
|
}
|
}
|
|
private long frozenSerialStart(String snapshot) throws DataException {
|
try {
|
JsonNode root = SNAPSHOT_MAPPER.readTree(snapshot);
|
if (root != null && !root.has("serialStart")) {
|
return 1L; // 历史快照沿用原来从1开始的发号规则。
|
}
|
JsonNode value = root == null ? null : root.get("serialStart");
|
if (value != null && value.isIntegralNumber() && value.canConvertToLong()
|
&& value.longValue() > 0) {
|
return value.longValue();
|
}
|
} catch (JsonProcessingException exception) {
|
throw new DataException("流水号起始值快照无效");
|
}
|
throw new DataException("流水号起始值快照无效");
|
}
|
|
private boolean isSerialType(String type) {
|
return "liushuihao".equalsIgnoreCase(type) || "流水号".equals(type);
|
}
|
|
private int parseSerialWidth(String width) throws DataException {
|
try {
|
int value = Integer.parseInt(width);
|
if (value < 1) {
|
throw new NumberFormatException();
|
}
|
return value;
|
} catch (NumberFormatException e) {
|
throw new DataException("流水号模板长度无效:" + width);
|
}
|
}
|
|
private String leftPad(Long value, int width) throws DataException {
|
String text = String.valueOf(value);
|
if (text.length() > width) {
|
throw new DataException("流水号超过规则配置长度");
|
}
|
return String.format("%0" + width + "d", value);
|
}
|
|
private void requireSingleUpdate(int affectedRows, String message) throws DataException {
|
if (affectedRows != 1) {
|
throw new DataException(message + ",请刷新后重试");
|
}
|
}
|
|
private String requireText(String value, String message) throws DataException {
|
String resolved = trim(value);
|
if (resolved.isEmpty()) {
|
throw new DataException(message);
|
}
|
return resolved;
|
}
|
|
private boolean blank(String value) {
|
return trim(value).isEmpty();
|
}
|
|
private String trim(String value) {
|
return value == null ? "" : value.trim();
|
}
|
|
private static final class RuleSnapshot {
|
private final String ruleName;
|
private final String template;
|
private final String preview;
|
private final boolean hasSerial;
|
private final boolean hasCustom;
|
private final String customHint;
|
private final String json;
|
|
private RuleSnapshot(String ruleName, String template, String preview,
|
boolean hasSerial, boolean hasCustom, String customHint, String json) {
|
this.ruleName = ruleName;
|
this.template = template;
|
this.preview = preview;
|
this.hasSerial = hasSerial;
|
this.hasCustom = hasCustom;
|
this.customHint = customHint;
|
this.json = json;
|
}
|
}
|
|
private static final class AssignedNumber {
|
private final String number;
|
private final String serial;
|
|
private AssignedNumber(String number, String serial) {
|
this.number = number;
|
this.serial = serial;
|
}
|
}
|
}
|