505 lines
27 KiB
Java
505 lines
27 KiB
Java
package com.yunhai.svchall.service.impl;
|
||
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import com.yunhai.platform.apistd.BizException;
|
||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||
import com.yunhai.platform.utils.model.BoaUser;
|
||
import com.yunhai.svchall.entity.ServiceApply;
|
||
import com.yunhai.svchall.entity.ServiceForm;
|
||
import com.yunhai.svchall.entity.ServiceItem;
|
||
import com.yunhai.svchall.enums.ApplyFlowStatusEnum;
|
||
import com.yunhai.svchall.enums.ServiceItemStatusEnum;
|
||
import com.yunhai.svchall.model.svc.SvcModels;
|
||
import com.yunhai.svchall.model.svc.SvcModels.ApplyPageQuery;
|
||
import com.yunhai.svchall.model.svc.SvcModels.ApplySubmitRequest;
|
||
import com.yunhai.svchall.model.svc.SvcModels.ApplyVo;
|
||
import com.yunhai.svchall.repository.ServiceApplyRepository;
|
||
import com.yunhai.svchall.repository.ServiceItemRepository;
|
||
import com.yunhai.svchall.service.FlowClient;
|
||
import com.yunhai.svchall.service.SiteDirectory;
|
||
import com.yunhai.svchall.service.SiteDirectory.SiteInfo;
|
||
import com.yunhai.svchall.service.SiteGuard;
|
||
import com.yunhai.svchall.utils.FormValidator;
|
||
import lombok.RequiredArgsConstructor;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.beans.factory.annotation.Value;
|
||
import org.springframework.data.domain.Page;
|
||
import org.springframework.data.domain.PageRequest;
|
||
import org.springframework.data.domain.Pageable;
|
||
import org.springframework.data.domain.Sort;
|
||
import org.springframework.data.jpa.domain.Specification;
|
||
import org.springframework.stereotype.Service;
|
||
import org.springframework.transaction.annotation.Transactional;
|
||
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
|
||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||
import org.springframework.util.StringUtils;
|
||
|
||
import javax.persistence.criteria.Predicate;
|
||
import java.time.LocalDateTime;
|
||
import java.util.ArrayList;
|
||
import java.util.HashMap;
|
||
import java.util.LinkedHashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.concurrent.ExecutorService;
|
||
import java.util.concurrent.Executors;
|
||
|
||
/**
|
||
* 办件服务(V2.0 Part B.4 + 验收报告 §四 D9/§四.2 范式对齐):用户发起(事项/表单快照落库 →
|
||
* AFTER_COMMIT 后独立线程 FlowClient 代发起 → 回调驱动镜像状态)、我的办件/详情、管理端分页、
|
||
* 重试(复用同一 bizId 命中 flow-center uk 幂等)。提交纪律:快照自包含(item_snapshot_json/
|
||
* form_data_json),事项后续改名/下架不影响在途与历史办件;代发起出站不进 DB 事务(HTTP 长调用
|
||
* 拖住连接 + 回调先于提交到达找不到行),成功回填走窄列 markApproving(终态不回退)。
|
||
* 用户面撤回/催办/审批轨迹由前端直调 flow-center CAS 接口(同主域 cookie),本服务不代理。
|
||
*/
|
||
@Slf4j
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
public class ServiceApplyService {
|
||
|
||
private final ServiceApplyRepository applyRepository;
|
||
private final ServiceItemRepository itemRepository;
|
||
private final ServiceFormService formService;
|
||
private final AttachmentService attachmentService;
|
||
private final FlowClient flowClient;
|
||
private final SiteGuard siteGuard;
|
||
private final SiteDirectory siteDirectory;
|
||
private final com.yunhai.svchall.service.ApplyViewGuard applyViewGuard;
|
||
private final ObjectMapper objectMapper;
|
||
private final org.springframework.transaction.support.TransactionTemplate transactionTemplate;
|
||
|
||
/** 办事大厅前端对外地址(bizUrl 落地页用;projman 注入,本地默认 dev server) */
|
||
@Value("${svchall.front.base-url:http://localhost:8087}")
|
||
private String frontBaseUrl;
|
||
|
||
// ===== 用户面(CAS)=====
|
||
|
||
/**
|
||
* 发起:本校成员 + 事项发布态 + 表单数据强校验 → 落 service_apply(PENDING, bizId=id)
|
||
* → AFTER_COMMIT 后独立线程代发起(失败保留 PENDING 行,重试复用同一 id)。
|
||
* 返回办件 VO(PENDING 态——代发起异步执行,状态秒级翻转,前端以"待提交/审批中"刷新可见)。
|
||
*/
|
||
@Transactional
|
||
public ApplyVo submit(BoaUser user, ApplySubmitRequest req) {
|
||
if (user == null || user.getId() == null) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "未获取到登录用户");
|
||
}
|
||
SiteInfo site = siteDirectory.requireSite(req.getSiteCode());
|
||
siteGuard.requireSchoolMember(user, site.getSchId());
|
||
ServiceItem item = itemRepository
|
||
.findBySchIdAndSiteCodeAndItemCode(site.getSchId(), site.getSiteCode(), req.getItemCode())
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "事项不存在"));
|
||
if (item.getStatus() == null || item.getStatus() != ServiceItemStatusEnum.PUBLISHED.getCode()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "事项未发布,不可办理");
|
||
}
|
||
if (item.getFlowGuid() == null || item.getFlowGuid().isEmpty()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "该事项未开通在线办理");
|
||
}
|
||
// 表单解析 + 提交数据强校验(按最新已发布版本;发起时快照 form_version)
|
||
ServiceForm form = null;
|
||
if (item.getFormCode() != null && !item.getFormCode().isEmpty()) {
|
||
form = formService.latestPublished(site.getSchId(), item.getFormCode())
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR,
|
||
"办理表单未发布,请联系管理员(事项 " + item.getItemCode() + ")"));
|
||
FormValidator.validateSubmit(req.getFormData(), form.getSchemaJson());
|
||
} else if (StringUtils.hasText(req.getFormData())) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "该事项无办理表单,不应提交表单数据");
|
||
}
|
||
|
||
ServiceApply apply = new ServiceApply();
|
||
apply.setSchId(site.getSchId());
|
||
apply.setSiteCode(site.getSiteCode());
|
||
apply.setItemId(item.getId());
|
||
apply.setItemCode(item.getItemCode());
|
||
apply.setItemTitle(item.getTitle());
|
||
apply.setItemSnapshotJson(itemSnapshotJson(item));
|
||
apply.setFormVersion(form == null ? null : form.getVersion());
|
||
apply.setFormDataJson(req.getFormData());
|
||
apply.setSubmitterId(user.getId());
|
||
apply.setSubmitterName(user.getName());
|
||
apply.setFlowGuid(item.getFlowGuid());
|
||
apply = applyRepository.save(apply);
|
||
|
||
// 办件材料绑定(必须落库拿到 applyId 之后再绑)
|
||
attachmentService.bindApply(site.getSchId(), apply.getId(), req.getAttachments());
|
||
apply.setItemSnapshotJson(itemSnapshotJson(item, attachmentService
|
||
.applyAttachments(site.getSchId(), apply.getId())));
|
||
apply = applyRepository.save(apply);
|
||
|
||
flowSubmitAfterCommit(apply.getId());
|
||
return stampFlowEnabled(ApplyVo.of(apply));
|
||
}
|
||
|
||
/** 重试:PENDING 行复用同一 bizId 再次代发起(幂等);仅提交人本人可重试自己的办件(同步路径,无长事务) */
|
||
public ApplyVo retry(BoaUser user, Integer applyId) {
|
||
ServiceApply apply = applyRepository.findByIdAndSchId(applyId, userSchId(user))
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||
if (!user.getId().equals(apply.getSubmitterId())) {
|
||
throw new BizException(ErrorCode.FORBIDDEN, "仅可重试自己的办件");
|
||
}
|
||
if (apply.getFlowStatus() == null || apply.getFlowStatus() != ApplyFlowStatusEnum.PENDING.getCode()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "该办件不在待提交状态,无需重试");
|
||
}
|
||
doFlowApply(applyId, false);
|
||
return stampFlowEnabled(applyRepository.findById(applyId).map(ApplyVo::of).orElse(ApplyVo.of(apply)));
|
||
}
|
||
|
||
/**
|
||
* 一键重提(P2-5/BP-R2-2):驳回(30)/撤回(40)后复用同一办件行重新发起——flow-center R4
|
||
* 按 bizType+bizId 命中 30/40 → 重启推进(定义刷新、快照覆盖、version 自增、轨迹跨轮连续)。
|
||
* 校验:提交人本人 + 终态 30/40 + flowGuid 非空;表单数据沿用原 formData
|
||
* ("改后重提"属后续产品项,登记于验收报告)。出站失败保持原态可重入。
|
||
*/
|
||
public ApplyVo resubmit(BoaUser user, Integer applyId) {
|
||
ServiceApply apply = applyRepository.findByIdAndSchId(applyId, userSchId(user))
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||
if (!user.getId().equals(apply.getSubmitterId())) {
|
||
throw new BizException(ErrorCode.FORBIDDEN, "仅可重提自己的办件");
|
||
}
|
||
Integer status = apply.getFlowStatus();
|
||
if (status == null || (status != ApplyFlowStatusEnum.REJECTED.getCode()
|
||
&& status != ApplyFlowStatusEnum.WITHDRAWN.getCode())) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "仅驳回/已撤回状态的办件可重新提交");
|
||
}
|
||
if (apply.getFlowGuid() == null || apply.getFlowGuid().isEmpty()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "该办件未绑定流程,不可重提");
|
||
}
|
||
doFlowApply(applyId, true);
|
||
return stampFlowEnabled(applyRepository.findById(applyId).map(ApplyVo::of).orElse(ApplyVo.of(apply)));
|
||
}
|
||
|
||
/**
|
||
* 撤回(用户面;2026-09-16 治理:原先前端直调 flow-center {@code /api/flow/instance/{id}/withdraw}
|
||
* ——跨应用 CAS 票据 + CORS 双重外溢,且"谁能撤"落在前端)。现由本服务中转:
|
||
* ①本应用内定位办件(同校 + 提交人本人 = 权威授权,也是列表自见规则);②状态须审批中(10) 且
|
||
* 已有流程实例;③经 FlowClient→SDK 门面调 flow-center 托管件(对端再按 submitterId 复核一次)。
|
||
* 撤回后的镜像状态由 flow.withdrawn 回调推进(40),本方法不改本地状态——单一写入口,
|
||
* 避免"本地先改 + 回调后到"的版本比较歧义。
|
||
*/
|
||
public void withdraw(BoaUser user, Integer applyId) {
|
||
ServiceApply apply = requireOwnApply(user, applyId, "仅可撤回自己的办件");
|
||
if (apply.getFlowStatus() == null
|
||
|| apply.getFlowStatus() != ApplyFlowStatusEnum.APPROVING.getCode()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "仅审批中的办件可撤回");
|
||
}
|
||
if (apply.getFlowInstanceId() == null) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "该办件尚未进入流程,无需撤回");
|
||
}
|
||
flowClient.withdraw(apply.getSchId(), apply.getFlowInstanceId(), apply.getSubmitterId());
|
||
}
|
||
|
||
/**
|
||
* 审批进度(结构化轨迹):可见性走与详情同一个守卫(本人/管理员/办理人三分支),
|
||
* 再按 instanceId 取流程中心进度——对端只校验 schId,故守卫必须在本地先做。
|
||
* 未进入流程的办件返回空轨迹(前端渲染"待提交"提示),不报错。
|
||
*/
|
||
public SvcModels.FlowProgressVo progress(BoaUser user, Integer applyId) {
|
||
ServiceApply apply = requireVisibleApply(user, applyId);
|
||
if (apply.getFlowInstanceId() == null) {
|
||
SvcModels.FlowProgressVo empty = new SvcModels.FlowProgressVo();
|
||
empty.setInstanceId(null);
|
||
empty.setBizType(FlowCallbackService.BIZ_TYPE);
|
||
empty.setBizId(String.valueOf(apply.getId()));
|
||
empty.setBizTitle(apply.getItemTitle());
|
||
empty.setStatus(apply.getFlowStatus());
|
||
empty.setSubmitterId(apply.getSubmitterId());
|
||
empty.setSubmitterName(apply.getSubmitterName());
|
||
empty.setSubmitTime(apply.getSubmitTime() == null ? null : apply.getSubmitTime().toString());
|
||
empty.setSteps(new ArrayList<>());
|
||
return empty;
|
||
}
|
||
return flowClient.progress(apply.getSchId(), apply.getFlowInstanceId(), null, null);
|
||
}
|
||
|
||
/**
|
||
* 流程示意图(PNG 流):同守卫;instanceId 口径(带运行时节点状态)。
|
||
* 出图两步(取免签 token → 取图字节)全部在本服务侧完成,浏览器只拿同源 {@code /api/**} 图片。
|
||
*/
|
||
public byte[] diagramImage(BoaUser user, Integer applyId, String mode) {
|
||
ServiceApply apply = requireVisibleApply(user, applyId);
|
||
if (apply.getFlowInstanceId() == null) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "该办件尚未进入流程,无流程图");
|
||
}
|
||
return flowClient.diagramImage(apply.getSchId(), apply.getFlowInstanceId(), null, mode);
|
||
}
|
||
|
||
/** 本人办件(提交人校验;撤回/重提共用口径) */
|
||
private ServiceApply requireOwnApply(BoaUser user, Integer applyId, String forbiddenMsg) {
|
||
ServiceApply apply = applyRepository.findByIdAndSchId(applyId, userSchId(user))
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||
if (!user.getId().equals(apply.getSubmitterId())) {
|
||
throw new BizException(ErrorCode.FORBIDDEN, forbiddenMsg);
|
||
}
|
||
return apply;
|
||
}
|
||
|
||
/** 可见办件(提交人本人 / 管理员 / 办理人;非超管) */
|
||
private ServiceApply requireVisibleApply(BoaUser user, Integer applyId) {
|
||
if (isSuperAdmin(user)) {
|
||
return applyRepository.findById(applyId)
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||
}
|
||
ServiceApply apply = applyRepository.findByIdAndSchId(applyId, userSchId(user))
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||
applyViewGuard.requireVisible(apply, user);
|
||
return apply;
|
||
}
|
||
|
||
/** 我的办件(提交人视角,flowStatus 可选筛选) */
|
||
public Map<String, Object> myApplies(BoaUser user, Integer flowStatus, Integer page, Integer pageSize) {
|
||
int pageIndex = page == null || page < 1 ? 0 : page - 1;
|
||
int size = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100);
|
||
Pageable pageable = PageRequest.of(pageIndex, size,
|
||
Sort.by(Sort.Order.desc("createTime"), Sort.Order.desc("id")));
|
||
Specification<ServiceApply> spec = (root, cq, cb) -> {
|
||
List<Predicate> predicates = new ArrayList<>();
|
||
predicates.add(cb.equal(root.get("schId"), userSchId(user)));
|
||
predicates.add(cb.equal(root.get("submitterId"), user.getId()));
|
||
if (flowStatus != null) {
|
||
predicates.add(cb.equal(root.get("flowStatus"), flowStatus));
|
||
}
|
||
return cb.and(predicates.toArray(new Predicate[0]));
|
||
};
|
||
Page<ServiceApply> result = applyRepository.findAll(spec, pageable);
|
||
Map<String, Object> out = new HashMap<>();
|
||
out.put("total", result.getTotalElements());
|
||
List<ApplyVo> list = new ArrayList<>();
|
||
for (ServiceApply apply : result.getContent()) {
|
||
list.add(ApplyVo.of(apply));
|
||
}
|
||
out.put("list", list);
|
||
return out;
|
||
}
|
||
|
||
/**
|
||
* 详情:提交人本人 / 管理员(type=1 或 roleId∈{2,5,7})/ 办理人(assigneeIds 实时含当前用户)
|
||
* 三分支放行(BP-R2-3 修复,ApplyViewGuard=NoticeFlowGuard 同构;原先"同校即放行"越权已收紧)。
|
||
*/
|
||
public ApplyVo detail(BoaUser user, String siteCode, Integer id) {
|
||
ServiceApply apply;
|
||
if (isSuperAdmin(user)) {
|
||
apply = applyRepository.findById(id)
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||
} else {
|
||
Long schId = userSchId(user);
|
||
apply = applyRepository.findByIdAndSchId(id, schId)
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||
applyViewGuard.requireVisible(apply, user);
|
||
}
|
||
return ApplyVo.of(apply);
|
||
}
|
||
|
||
// ===== 管理面 =====
|
||
|
||
/**
|
||
* 发起页数据(本校用户):事项发布态要点 + 绑定表单的已发布 schema。
|
||
* 校验链与 submit 一致(同校成员/发布态/已绑流程),前端据此渲染动态表单。
|
||
*/
|
||
public Map<String, Object> applyItem(BoaUser user, String siteCode, String itemCode) {
|
||
SiteInfo site = siteDirectory.requireSite(siteCode);
|
||
siteGuard.requireSchoolMember(user, site.getSchId());
|
||
ServiceItem item = itemRepository
|
||
.findBySchIdAndSiteCodeAndItemCode(site.getSchId(), site.getSiteCode(), itemCode)
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "事项不存在"));
|
||
if (item.getStatus() == null || item.getStatus() != ServiceItemStatusEnum.PUBLISHED.getCode()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "事项未发布,不可办理");
|
||
}
|
||
if (item.getFlowGuid() == null || item.getFlowGuid().isEmpty()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "该事项未开通在线办理");
|
||
}
|
||
Map<String, Object> out = new HashMap<>();
|
||
out.put("siteCode", site.getSiteCode());
|
||
out.put("itemCode", item.getItemCode());
|
||
out.put("title", item.getTitle());
|
||
out.put("summary", item.getSummary());
|
||
out.put("deptName", item.getDeptName());
|
||
out.put("handleMode", item.getHandleMode());
|
||
out.put("handleTimeLimit", item.getHandleTimeLimit());
|
||
out.put("promiseTimeLimit", item.getPromiseTimeLimit());
|
||
out.put("contactPhone", item.getContactPhone());
|
||
out.put("coverImage", com.yunhai.svchall.utils.FileUtil.absolutizePlain(item.getCoverImage()));
|
||
out.put("flowGuid", item.getFlowGuid());
|
||
if (item.getFormCode() != null && !item.getFormCode().isEmpty()) {
|
||
ServiceForm form = formService.latestPublished(site.getSchId(), item.getFormCode())
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR,
|
||
"办理表单未发布,请联系管理员"));
|
||
out.put("formVersion", form.getVersion());
|
||
out.put("schemaJson", form.getSchemaJson());
|
||
}
|
||
return out;
|
||
}
|
||
|
||
public Map<String, Object> adminPage(BoaUser user, ApplyPageQuery query) {
|
||
SiteInfo site = siteGuard.requireSiteManager(user, query.getSiteCode());
|
||
int pageIndex = query.getPage() == null || query.getPage() < 1 ? 0 : query.getPage() - 1;
|
||
int size = query.getPageSize() == null || query.getPageSize() < 1 ? 10 : Math.min(query.getPageSize(), 100);
|
||
Pageable pageable = PageRequest.of(pageIndex, size,
|
||
Sort.by(Sort.Order.desc("createTime"), Sort.Order.desc("id")));
|
||
Specification<ServiceApply> spec = (root, cq, cb) -> {
|
||
List<Predicate> predicates = new ArrayList<>();
|
||
predicates.add(cb.equal(root.get("schId"), site.getSchId()));
|
||
predicates.add(cb.equal(root.get("siteCode"), site.getSiteCode()));
|
||
if (StringUtils.hasText(query.getItemCode())) {
|
||
predicates.add(cb.equal(root.get("itemCode"), query.getItemCode().trim()));
|
||
}
|
||
if (query.getFlowStatus() != null) {
|
||
predicates.add(cb.equal(root.get("flowStatus"), query.getFlowStatus()));
|
||
}
|
||
if (StringUtils.hasText(query.getKeyword())) {
|
||
String like = "%" + query.getKeyword().trim() + "%";
|
||
predicates.add(cb.or(cb.like(root.get("itemTitle"), like),
|
||
cb.like(root.get("submitterName"), like)));
|
||
}
|
||
return cb.and(predicates.toArray(new Predicate[0]));
|
||
};
|
||
Page<ServiceApply> result = applyRepository.findAll(spec, pageable);
|
||
Map<String, Object> out = new HashMap<>();
|
||
out.put("total", result.getTotalElements());
|
||
List<ApplyVo> list = new ArrayList<>();
|
||
for (ServiceApply apply : result.getContent()) {
|
||
list.add(ApplyVo.of(apply));
|
||
}
|
||
out.put("list", list);
|
||
return out;
|
||
}
|
||
|
||
// ===== 内部:代发起(D9 范式,Notice NoticeFlowService 同构)=====
|
||
|
||
/**
|
||
* 提交动作专用单线程池(串行化避免并发双 apply;uk 幂等兜底)。
|
||
* E2E 实测教训(验收报告 §四.1):afterCommit 回调若留在原请求线程执行,OSIV 绑定的
|
||
* EntityManager 已随事务提交关闭,后续 repository 写会话静默丢失——提交动作一律切独立线程。
|
||
*/
|
||
private static final ExecutorService APPLY_EXECUTOR = Executors.newSingleThreadExecutor(r -> {
|
||
Thread t = new Thread(r, "svchall-flow-submit");
|
||
t.setDaemon(true);
|
||
return t;
|
||
});
|
||
|
||
/**
|
||
* 事务提交后再代发起(出站 HTTP 不进 DB 事务,防长调用拖住连接;且 flow-center 收到 apply
|
||
* 后会立即推送回调,须保证办件行已提交可见)。无活动事务时立即执行(测试/兜底)。
|
||
*/
|
||
private void flowSubmitAfterCommit(Integer applyId) {
|
||
Runnable task = () -> tryApplyQuietly(applyId);
|
||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
|
||
@Override
|
||
public void afterCommit() {
|
||
APPLY_EXECUTOR.submit(task);
|
||
}
|
||
});
|
||
} else {
|
||
task.run();
|
||
}
|
||
}
|
||
|
||
/** 异步路径提交失败静默保持 PENDING(行已落库不回滚;列表"待提交"态即提示,可重试) */
|
||
private void tryApplyQuietly(Integer applyId) {
|
||
try {
|
||
doFlowApply(applyId, false);
|
||
} catch (Exception e) {
|
||
log.warn("[svc-hall] 代发起失败(保持待提交,可重试)applyId={}:{}", applyId, e.toString());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 代发起(幂等:bizId=apply.id,flow-center uk 防重复建件)。
|
||
* 成功回填走窄列 markApproving——整实体 save 是旧快照盲写,会覆盖出站期间并发送达的回调状态
|
||
* (验收报告 §四.2);BizException 透传(retry 同步路径给用户提示,异步路径由静默包装消化)。
|
||
* resubmit=true(一键重提):R4 命中 30/40 重启推进后改用 markResubmitted 回开为 10。
|
||
*/
|
||
private void doFlowApply(Integer applyId, boolean resubmit) {
|
||
if (!flowClient.enabled()) {
|
||
log.warn("[svc-hall] 流程中心未接入,办件保留待提交状态 applyId={}", applyId);
|
||
return;
|
||
}
|
||
ServiceApply apply = applyRepository.findById(applyId)
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
|
||
try {
|
||
FlowClient.ApplyResult result = flowClient.apply(
|
||
apply.getSchId(),
|
||
apply.getFlowGuid(),
|
||
FlowCallbackService.BIZ_TYPE,
|
||
String.valueOf(apply.getId()),
|
||
apply.getSubmitterName() + "的" + apply.getItemTitle(),
|
||
// 前端详情页是 hash 路由 + query 传参(routes.ts path="/apply-detail"、
|
||
// Apply.vue router.replace("/apply-detail?id=")):路径参数形态会 no-match 空白页
|
||
frontBaseUrl + "/#/apply-detail?id=" + apply.getId(),
|
||
apply.getItemSnapshotJson(),
|
||
apply.getSubmitterId(),
|
||
apply.getSubmitterName(),
|
||
apply.getFormDataJson());
|
||
// @Modifying 更新须在事务内执行;本方法出站 HTTP 后运行在 AFTER_COMMIT 线程,
|
||
// 无外层事务,用模板事务包住窄列回填( markApproving/markResubmitted)
|
||
final Integer applyIdFinal = apply.getId();
|
||
final Long instanceId = result.getInstanceId();
|
||
transactionTemplate.executeWithoutResult(tx -> {
|
||
if (resubmit) {
|
||
applyRepository.markResubmitted(applyIdFinal, instanceId, LocalDateTime.now());
|
||
} else {
|
||
applyRepository.markApproving(applyIdFinal, instanceId, LocalDateTime.now());
|
||
}
|
||
});
|
||
if (result.isDuplicated()) {
|
||
log.info("[svc-hall] 代发起命中幂等(flow-center 返回既有办件)applyId={} instanceId={}",
|
||
apply.getId(), result.getInstanceId());
|
||
}
|
||
} catch (BizException e) {
|
||
throw e;
|
||
} catch (Exception e) {
|
||
// PENDING 行保留:用户可重试(复用同一 bizId,flow-center uk 幂等防重复建件)
|
||
log.error("[svc-hall] 代发起失败,办件保留待提交 applyId={}", applyId, e);
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "流程中心提交失败,请稍后重试");
|
||
}
|
||
}
|
||
|
||
// ===== 内部:快照与工具 =====
|
||
|
||
/**
|
||
* 提交类响应补"流程中心是否已接入"标志(BP-R4-1 误报根因,响应期字段不入库):
|
||
* 代发起是 AFTER_COMMIT 异步,提交成功那一刻 flowStatus 必然是 0;只有结合本标志,
|
||
* 前端才能区分"排队中(秒级翻转,等轮询)"与"压根没接入(不会翻转,需提示重试/联系人)"。
|
||
*/
|
||
private ApplyVo stampFlowEnabled(ApplyVo vo) {
|
||
vo.setFlowEnabled(flowClient.enabled());
|
||
return vo;
|
||
}
|
||
|
||
/** 事项要点快照(= biz_snapshot 内容源;附件清单仅文件名——flow-center biz_snapshot 内容边界) */
|
||
private String itemSnapshotJson(ServiceItem item) {
|
||
return itemSnapshotJson(item, new ArrayList<>());
|
||
}
|
||
|
||
private String itemSnapshotJson(ServiceItem item, List<com.yunhai.svchall.model.svc.SvcModels.SvcItemAttachmentVo> attachments) {
|
||
Map<String, Object> snapshot = new LinkedHashMap<>();
|
||
snapshot.put("itemTitle", item.getTitle());
|
||
snapshot.put("summary", item.getSummary());
|
||
snapshot.put("deptName", item.getDeptName());
|
||
snapshot.put("handleTimeLimit", item.getHandleTimeLimit());
|
||
snapshot.put("contactPhone", item.getContactPhone());
|
||
List<String> names = new ArrayList<>();
|
||
for (com.yunhai.svchall.model.svc.SvcModels.SvcItemAttachmentVo a : attachments) {
|
||
names.add(a.getFileName());
|
||
}
|
||
snapshot.put("attachmentNames", names);
|
||
try {
|
||
return objectMapper.writeValueAsString(snapshot);
|
||
} catch (Exception e) {
|
||
log.warn("[svc-hall] 事项快照序列化失败(存 null)itemCode={}", item.getItemCode());
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private Long userSchId(BoaUser user) {
|
||
if (user == null || user.getCurrentRole() == null || user.getCurrentRole().getSchId() == null) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "当前用户无学校归属,无法操作办件");
|
||
}
|
||
return user.getCurrentRole().getSchId().longValue();
|
||
}
|
||
|
||
private static boolean isSuperAdmin(BoaUser user) {
|
||
return user != null && user.getType() != null && user.getType() == 1;
|
||
}
|
||
}
|