[v2.1] A 应用内审批地基:oapi 增 GET /tasks/by-biz——按单据查当前人待办(0/1/N 均 200 空数组不泄露,校 schId/bizType/assignee 三 match,IT 11 例)
build / build-push (push) Successful in 2m2s

This commit is contained in:
chenxing
2026-10-19 16:44:35 +08:00
parent 8ceb2a4247
commit 132919b029
4 changed files with 385 additions and 0 deletions
@@ -32,6 +32,7 @@ import java.util.List;
* 应用内审批(2026-09 第三步,原"刻意不提供审批动作"立场由该方案修订:登录身份由调用方
* 应用按其 CAS 会话保证并以 operatorId 传入,本端按任务办理人逐字复核,审计链不断):
* - GET /oapi/flow/tasks/todo?schId=&assigneeId=&bizType=&page=&size= → data=分页待办
* - GET /oapi/flow/tasks/by-biz?schId=&bizType=&bizId=&operatorId= → data=当前人在该单据的待办(0/1/N)
* - GET /oapi/flow/instances?schId=&bizType=&status=&page=&size= → data=分页办件
* - POST /oapi/flow/task/{taskId}/approve?schId=&operatorId=&operatorName=(body: opinion/edgeName)
* - POST /oapi/flow/task/{taskId}/reject?schId=&operatorId=&operatorName=(body: opinion 必填)
@@ -158,6 +159,20 @@ public class OapiFlowController {
return flowService.pageTodoTasks(schId, assigneeId, bizType, page, size);
}
/**
* 按业务单据查当前人待办(v2.1 业务详情卡片数据源):与 tasks/todo 互补——那个是"人→单据",
* 这个是"单据→我的待办"。0/1/N 三种计数都由 HTTP 200 + tasks 数组表达,办件不存在与
* 他人待办同样出空(不用于探测)。operatorId 由子应用后端从 CAS 会话取,前端不可传。
*/
@GetMapping("/tasks/by-biz")
public OapiModels.ByBizTasksVo tasksByBiz(
@RequestParam("schId") Long schId,
@RequestParam("bizType") String bizType,
@RequestParam("bizId") String bizId,
@RequestParam("operatorId") Integer operatorId) {
return flowService.tasksByBiz(schId, bizType, bizId, operatorId);
}
/** 办件分页(oapi):bizType/status 可选过滤 */
@GetMapping("/instances")
public BasePageResult<OapiModels.InstanceItemVo> instances(
@@ -3,6 +3,7 @@ package com.yunhai.flowcenter.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -266,4 +267,45 @@ public final class OapiModels {
/** 多出口节点办理时所选出口名(单出口可省) */
private String edgeName;
}
/**
* 按业务单据定位的当前人待办(tasks/by-biz 出参,v2.1 应用内审批卡片数据源)。
* 无办件与"有办件但本人无待办"同为 tasks=[] + HTTP 200,出参不区分二者(防探测)。
*/
@Data
public static class ByBizTasksVo {
/** 办件 id;无办件时 null */
private Long instanceId;
private String bizType;
private String bizId;
/** 办件状态;无办件时 null */
private Integer status;
/**
* 办件当前活跃节点——**节点 id** 逗号串,原文照抄 {@code flw_instance.current_nodes},
* 与 {@code instances}/{@code progress} 同口径(不是节点名,渲染人读名称请取
* {@code tasks[].nodeName})。无办件时 null。
*/
private String currentNodes;
/** 当前人 PENDING 任务,按 taskId 升序;无待办为空数组 */
private List<ByBizTaskVo> tasks = new ArrayList<>();
}
/** by-biz 单条待办:只含办理该任务必需的定位信息,不透出他人办理人/意见 */
@Data
public static class ByBizTaskVo {
private Long taskId;
private String nodeId;
private String nodeName;
private String arriveTime;
/** 出口列表;单出口节点为空数组(与 FlowQueryService.detail 的 outlets 口径一致) */
private List<ByBizOutletVo> outlets = new ArrayList<>();
}
/** by-biz 出口(多出口节点办理时须选其一,透传给 approve 的 edgeName) */
@Data
public static class ByBizOutletVo {
private String name;
private String toNodeId;
private String toNodeName;
}
}
@@ -349,6 +349,79 @@ public class OapiFlowService {
return toPageResult(p.getTotalElements(), items, page, size);
}
/**
* 按业务单据查当前人待办(v2.1 应用内审批的业务详情卡片数据源):
* 补齐 tasks/todo 缺的那一侧——todo 是"人 → 单据列表",本方法是"单据 → 我的待办",
* 子应用据此判断"当前人是否该办件的待办人",不必再拉全量待办在前端过滤。
* <p>治理口径:
* <ul>
* <li>只返回 {@code assigneeId=operatorId} 且 status=PENDING 的任务;他人待办与"办件不存在"
* 一律出空数组(HTTP 200),出参不区分二者,避免用本端点探测别人的办件;</li>
* <li>租户收敛在定位查询本身({@code findBySchIdAndBizTypeAndBizId}),bizType 不匹配即空;</li>
* <li>{@code outlets} 只为当前人的任务计算,且出边 &gt;1 才透出(与
* {@code FlowQueryService.detail} 的办理面板口径一致——单出口无需选择,不给前端多余分支);</li>
* <li>不透出他人办理人、意见或定义内部 id;taskId 是办理必需的运行时身份,保留。</li>
* </ul>
*/
public OapiModels.ByBizTasksVo tasksByBiz(Long schId, String bizType, String bizId, Integer operatorId) {
if (schId == null || schId <= 0 || operatorId == null || operatorId <= 0) {
throw new BizException(ErrorCode.PARAM_ERROR, "缺少必要参数:schId/operatorId(均须为正)");
}
String cleanBizType = emptyToNull(bizType);
String cleanBizId = emptyToNull(bizId);
if (cleanBizType == null || cleanBizId == null) {
throw new BizException(ErrorCode.PARAM_ERROR, "缺少必要参数:bizType/bizId");
}
OapiModels.ByBizTasksVo vo = new OapiModels.ByBizTasksVo();
vo.setBizType(cleanBizType);
vo.setBizId(cleanBizId);
FlwInstance instance = instanceRepository
.findBySchIdAndBizTypeAndBizId(schId, cleanBizType, cleanBizId).orElse(null);
if (instance == null) {
return vo;
}
vo.setInstanceId(instance.getId());
vo.setStatus(instance.getStatus());
vo.setCurrentNodes(instance.getCurrentNodes());
List<FlwTask> mine = taskRepository
.findByInstanceIdAndStatus(instance.getId(), FlwEnums.TaskStatus.PENDING.getCode()).stream()
.filter(t -> operatorId.equals(t.getAssigneeId()))
.sorted(java.util.Comparator.comparing(FlwTask::getId))
.collect(java.util.stream.Collectors.toList());
if (mine.isEmpty()) {
return vo;
}
FlwDef def = defRepository.findById(instance.getDefId()).orElse(null);
FlowDefModel model = def == null ? null : flowEngine.parseModel(def);
for (FlwTask t : mine) {
vo.getTasks().add(toByBizTaskVo(t, model));
}
return vo;
}
private OapiModels.ByBizTaskVo toByBizTaskVo(FlwTask t, FlowDefModel model) {
OapiModels.ByBizTaskVo vo = new OapiModels.ByBizTaskVo();
vo.setTaskId(t.getId());
vo.setNodeId(t.getNodeId());
vo.setNodeName(t.getNodeName());
vo.setArriveTime(t.getArriveTime() == null ? null : t.getArriveTime().toString());
if (model == null) {
return vo;
}
List<FlowDefModel.Edge> outs = model.outEdges(t.getNodeId());
if (outs.size() > 1) {
for (FlowDefModel.Edge e : outs) {
FlowDefModel.Node to = model.node(e.to);
OapiModels.ByBizOutletVo outlet = new OapiModels.ByBizOutletVo();
outlet.setName(e.name);
outlet.setToNodeId(e.to);
outlet.setToNodeName(to == null || to.name == null || to.name.isEmpty() ? e.to : to.name);
vo.getOutlets().add(outlet);
}
}
return vo;
}
/**
* 同意(oapi,应用内审批):operatorId 由调用方应用按其登录态传入,本端按任务办理人
* 复核({@code task.assigneeId == operatorId},不满足→业务码拒绝,应用签名不替代"谁在办")。