feat(oapi): 应用内审批 oapi——待办/办件查询 + 办理(operator 复核)+ 待办跳转 bizUrl 优先
build / build-push (push) Successful in 1m29s
build / build-push (push) Successful in 1m29s
- GET /oapi/flow/tasks/todo?schId&assigneeId&bizType&page&size(出参含 biz 定位/快照/版本)
- GET /oapi/flow/instances?schId&bizType&status&page&size(查询不限办理人)
- POST /oapi/flow/task/{taskId}/approve|reject?schId&operatorId&operatorName:
operatorId 按任务办理人逐字复核(非办理人→业务码拒绝不 500),拒绝必填意见,
opinion 经事件链透传到回调 payload;审批后回调/待办减一自动复用
- 待办通知 jumpPath:办件 bizUrl 优先(空回落 /task/{taskId})
契约 IT +3(todo 过滤分页/instances 状态过滤/办理三态+payload 断言),
单测 UserMsgNotifierBizUrlTest 两态;全量 99 单测 + 11 契约 IT 绿。
This commit is contained in:
@@ -2,6 +2,7 @@ package com.yunhai.flowcenter.controller.oapi;
|
||||
|
||||
import com.yunhai.flowcenter.model.OapiModels;
|
||||
import com.yunhai.flowcenter.service.OapiFlowService;
|
||||
import com.yunhai.platform.utils.model.BasePageResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -25,8 +26,12 @@ import java.util.List;
|
||||
* - GET /oapi/flow/progress?instanceId=|bizType=&bizId=&schId= → data={status,steps:[…]}
|
||||
* - GET /oapi/flow/diagram/url?instanceId=|flowGuid=&mode=&schId= → data={imgUrl,expiresIn}
|
||||
* - POST /oapi/flow/callback/reg?schId= → data={secret}
|
||||
* 刻意不提供审批动作(同意/拒绝/退回/移交/催办):那是"人"的决策,必须带登录身份走 CAS 用户面,
|
||||
* 应用间签名不得代签——谁办理谁负责,审计链不能断在应用上。
|
||||
* 应用内审批(2026-09 第三步,原"刻意不提供审批动作"立场由该方案修订:登录身份由调用方
|
||||
* 应用按其 CAS 会话保证并以 operatorId 传入,本端按任务办理人逐字复核,审计链不断):
|
||||
* - GET /oapi/flow/tasks/todo?schId=&assigneeId=&bizType=&page=&size= → data=分页待办
|
||||
* - 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 必填)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/oapi/flow")
|
||||
@@ -102,4 +107,51 @@ public class OapiFlowController {
|
||||
@RequestBody OapiModels.CallbackRegBody body) {
|
||||
return flowService.registerCallback(schId, appKey, body);
|
||||
}
|
||||
|
||||
// ===== 应用内审批(第三步):查询 + 办理 =====
|
||||
|
||||
/** 我的待办(oapi):assigneeId 维度分页,bizType 可选过滤 */
|
||||
@GetMapping("/tasks/todo")
|
||||
public BasePageResult<OapiModels.TodoTaskVo> todoTasks(
|
||||
@RequestParam("schId") Long schId,
|
||||
@RequestParam("assigneeId") Integer assigneeId,
|
||||
@RequestParam(value = "bizType", required = false) String bizType,
|
||||
@RequestParam(value = "page", required = false) Integer page,
|
||||
@RequestParam(value = "size", required = false) Integer size) {
|
||||
return flowService.pageTodoTasks(schId, assigneeId, bizType, page, size);
|
||||
}
|
||||
|
||||
/** 办件分页(oapi):bizType/status 可选过滤 */
|
||||
@GetMapping("/instances")
|
||||
public BasePageResult<OapiModels.InstanceItemVo> instances(
|
||||
@RequestParam("schId") Long schId,
|
||||
@RequestParam(value = "bizType", required = false) String bizType,
|
||||
@RequestParam(value = "status", required = false) Integer status,
|
||||
@RequestParam(value = "page", required = false) Integer page,
|
||||
@RequestParam(value = "size", required = false) Integer size) {
|
||||
return flowService.pageInstances(schId, bizType, status, page, size);
|
||||
}
|
||||
|
||||
/** 同意(oapi):operatorId 按任务办理人逐字复核,非办理人→业务码拒绝 */
|
||||
@PostMapping("/task/{taskId}/approve")
|
||||
public void approve(@RequestParam("schId") Long schId,
|
||||
@RequestParam("operatorId") Integer operatorId,
|
||||
@RequestParam("operatorName") String operatorName,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@RequestBody(required = false) OapiModels.HandleBody body) {
|
||||
flowService.approveTask(schId, taskId, operatorId, operatorName,
|
||||
body == null ? null : body.getOpinion(),
|
||||
body == null ? null : body.getEdgeName());
|
||||
}
|
||||
|
||||
/** 拒绝(oapi):opinion 必填,非办理人→业务码拒绝 */
|
||||
@PostMapping("/task/{taskId}/reject")
|
||||
public void reject(@RequestParam("schId") Long schId,
|
||||
@RequestParam("operatorId") Integer operatorId,
|
||||
@RequestParam("operatorName") String operatorName,
|
||||
@PathVariable("taskId") Long taskId,
|
||||
@RequestBody(required = false) OapiModels.HandleBody body) {
|
||||
flowService.rejectTask(schId, taskId, operatorId, operatorName,
|
||||
body == null ? null : body.getOpinion());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,4 +145,47 @@ public final class OapiModels {
|
||||
/** 相对 token 有效期(秒) */
|
||||
private long expiresIn;
|
||||
}
|
||||
|
||||
/** 待办任务行(tasks/todo 元素):审批列表页数据源,带办件定位与快照 */
|
||||
@Data
|
||||
public static class TodoTaskVo {
|
||||
private Long taskId;
|
||||
private Long instanceId;
|
||||
/** 办件当前版本(办理前对账用) */
|
||||
private Integer instanceVersion;
|
||||
private String bizType;
|
||||
private String bizId;
|
||||
private String bizTitle;
|
||||
/** 办件快照原文(JSON 字符串,发起时未传则 null) */
|
||||
private String bizSnapshot;
|
||||
private String nodeId;
|
||||
private String nodeName;
|
||||
private Integer assigneeId;
|
||||
private String assigneeName;
|
||||
private String arriveTime;
|
||||
}
|
||||
|
||||
/** 办件行(instances 元素):应用侧列表/对账数据源 */
|
||||
@Data
|
||||
public static class InstanceItemVo {
|
||||
private Long instanceId;
|
||||
private Integer status;
|
||||
private Integer version;
|
||||
private String bizType;
|
||||
private String bizId;
|
||||
private String bizTitle;
|
||||
private String bizUrl;
|
||||
private String submitterName;
|
||||
private String submitTime;
|
||||
private String finishTime;
|
||||
private String currentNodes;
|
||||
}
|
||||
|
||||
/** 办理入参(approve/reject 请求体;opinion 可空,拒绝侧服务层强制必填) */
|
||||
@Data
|
||||
public static class HandleBody {
|
||||
private String opinion;
|
||||
/** 多出口节点办理时所选出口名(单出口可省) */
|
||||
private String edgeName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 站内信/待办角标通知(概览 §七 契约,回调通知"业务系统"、本类通知"人",两通道互不替代):
|
||||
* - 任务到达:msgType=2、bizType=flow.task、msgNo=task:{taskId}、jumpPath=/task/{taskId}、todo +1
|
||||
* - 任务到达:msgType=2、bizType=flow.task、msgNo=task:{taskId}、jumpPath=bizUrl 优先(空回落
|
||||
* /task/{taskId})、todo +1
|
||||
* - 任务被办理:todo -1(办理人)
|
||||
* - 完成/拒绝/撤回:msgType=1、bizType=flow.result、content 必填(结果+意见)、msgNo 带 version
|
||||
* (重提重启多轮终态互不幂等吞掉)
|
||||
@@ -68,10 +69,14 @@ public class UserMsgNotifier {
|
||||
String title = "您有新的待办:" + safe(instance.getBizTitle());
|
||||
String content = "「" + safe(instance.getBizTitle()) + "」等待您审批"
|
||||
+ (task.getNodeName() == null ? "" : "(" + task.getNodeName() + ")");
|
||||
// 跳转路径:办件自带 bizUrl(应用详情页)优先——应用内审批免跳流程中心;
|
||||
// 未传 bizUrl 回落流程中心办理页 /task/{taskId}
|
||||
String jumpPath = instance.getBizUrl() == null || instance.getBizUrl().trim().isEmpty()
|
||||
? "/task/" + task.getId() : instance.getBizUrl();
|
||||
userMsgClient.sendUserMsg(instance.getSchId().intValue(),
|
||||
Collections.singletonList(task.getAssigneeId()), 2,
|
||||
"flow.task", String.valueOf(instance.getId()), title, content,
|
||||
"/task/" + task.getId(), "task:" + task.getId(), 30);
|
||||
jumpPath, "task:" + task.getId(), 30);
|
||||
userMsgClient.todoInc(instance.getSchId().intValue(),
|
||||
Collections.singletonList(task.getAssigneeId()), 1);
|
||||
}
|
||||
|
||||
@@ -29,4 +29,11 @@ public interface FlwInstanceRepository extends JpaRepository<FlwInstance, Long>
|
||||
+ "and (:keyword is null or i.bizTitle like concat('%', :keyword, '%')) order by i.id desc")
|
||||
Page<FlwInstance> pageAdmin(@Param("schId") Long schId, @Param("status") Integer status,
|
||||
@Param("keyword") String keyword, Pageable pageable);
|
||||
|
||||
/** 按业务类型/状态分页(oapi instances 查询;bizType/status 均可空=不过滤) */
|
||||
@Query("select i from FlwInstance i where i.schId = :schId "
|
||||
+ "and (:bizType is null or i.bizType = :bizType) "
|
||||
+ "and (:status is null or i.status = :status) order by i.id desc")
|
||||
Page<FlwInstance> pageByBiz(@Param("schId") Long schId, @Param("bizType") String bizType,
|
||||
@Param("status") Integer status, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,17 @@ public interface FlwTaskRepository extends JpaRepository<FlwTask, Long> {
|
||||
+ "order by t.arriveTime asc, t.id asc")
|
||||
Page<FlwTask> pageTodo(@Param("schId") Long schId, @Param("assigneeId") Integer assigneeId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 我的待办(oapi 版,bizType 可选过滤):join 办件表过滤业务类型,排序口径与 {@link #pageTodo}
|
||||
* 一致(到达时间→id)。bizType 传 null 即全量(与 pageTodo 等价)。
|
||||
*/
|
||||
@Query("select t from FlwTask t join FlwInstance i on i.id = t.instanceId "
|
||||
+ "where t.schId = :schId and t.assigneeId = :assigneeId and t.status = 0 "
|
||||
+ "and (:bizType is null or i.bizType = :bizType) "
|
||||
+ "order by t.arriveTime asc, t.id asc")
|
||||
Page<FlwTask> pageTodoBiz(@Param("schId") Long schId, @Param("assigneeId") Integer assigneeId,
|
||||
@Param("bizType") String bizType, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 我的已办(一个办件一行,2026-09-18):同一办件被本人办理多次(多节点/退回重办/移交后
|
||||
* 再办)时只保留本人最后办理的那一条(该办件下本人已办行的最大 id),避免列表里同一办件
|
||||
|
||||
@@ -15,11 +15,14 @@ import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BasePageResult;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import com.yunhai.platform.openapi.base.OpenApiUserClient;
|
||||
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.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -61,6 +64,7 @@ public class OapiFlowService {
|
||||
private final FlwTaskRepository taskRepository;
|
||||
private final FlwCallbackRegRepository regRepository;
|
||||
private final FlowInstanceService instanceService;
|
||||
private final FlowTaskService taskService;
|
||||
private final FlowEngine flowEngine;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final OpenApiUserClient userClient;
|
||||
@@ -119,6 +123,133 @@ public class OapiFlowService {
|
||||
return instance == null ? null : toInstanceVo(instance);
|
||||
}
|
||||
|
||||
// ===== 应用内审批(2026-09 第三步):查询 + 办理 =====
|
||||
|
||||
/**
|
||||
* 我的待办(oapi,应用内审批列表页数据源):assigneeId 维度分页,bizType 可选过滤。
|
||||
* 办理人判定以 taskId 为准——每条待办任务行恰好一个 assignee,不再做集合判断。
|
||||
*/
|
||||
public BasePageResult<OapiModels.TodoTaskVo> pageTodoTasks(Long schId, Integer assigneeId,
|
||||
String bizType, Integer page, Integer size) {
|
||||
if (schId == null || assigneeId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "缺少必要参数:schId/assigneeId");
|
||||
}
|
||||
Page<FlwTask> p = taskRepository.pageTodoBiz(schId, assigneeId, emptyToNull(bizType),
|
||||
PageRequest.of(normPage(page), normSize(size)));
|
||||
List<Long> instanceIds = p.getContent().stream()
|
||||
.map(FlwTask::getInstanceId).distinct().collect(java.util.stream.Collectors.toList());
|
||||
Map<Long, FlwInstance> instances = instanceIds.isEmpty()
|
||||
? java.util.Collections.emptyMap()
|
||||
: instanceRepository.findAllById(instanceIds).stream()
|
||||
.collect(java.util.stream.Collectors.toMap(FlwInstance::getId, i -> i));
|
||||
List<OapiModels.TodoTaskVo> items = p.getContent().stream()
|
||||
.map(t -> toTodoTaskVo(t, instances.get(t.getInstanceId())))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
return toPageResult(p.getTotalElements(), items, page, size);
|
||||
}
|
||||
|
||||
/** 办件分页(oapi,应用侧列表/对账):bizType/status 可选过滤 */
|
||||
public BasePageResult<OapiModels.InstanceItemVo> pageInstances(Long schId, String bizType, Integer status,
|
||||
Integer page, Integer size) {
|
||||
if (schId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "缺少必要参数:schId");
|
||||
}
|
||||
Page<FlwInstance> p = instanceRepository.pageByBiz(schId, emptyToNull(bizType), status,
|
||||
PageRequest.of(normPage(page), normSize(size)));
|
||||
List<OapiModels.InstanceItemVo> items = p.getContent().stream()
|
||||
.map(this::toInstanceItemVo).collect(java.util.stream.Collectors.toList());
|
||||
return toPageResult(p.getTotalElements(), items, page, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同意(oapi,应用内审批):operatorId 由调用方应用按其登录态传入,本端按任务办理人
|
||||
* 复核({@code task.assigneeId == operatorId},不满足→业务码拒绝,应用签名不替代"谁在办")。
|
||||
* operatorName 供应用侧审计透传,事件流仍以任务行 assigneeName 为准。多出口节点必带
|
||||
* edgeName({@code FlowTaskService.handle} 内校验)。审批后的回调/待办减一等事件链自动复用。
|
||||
*/
|
||||
@Transactional
|
||||
public FlwTask approveTask(Long schId, Long taskId, Integer operatorId, String operatorName,
|
||||
String opinion, String edgeName) {
|
||||
requireOperator(schId, taskId, operatorId, operatorName);
|
||||
return taskService.handle(schId, taskId, true, opinion, operatorId, edgeName);
|
||||
}
|
||||
|
||||
/** 拒绝(oapi):opinion 必填由 {@code FlowTaskService.handle} 强制;其余口径同 {@link #approveTask} */
|
||||
@Transactional
|
||||
public FlwTask rejectTask(Long schId, Long taskId, Integer operatorId, String operatorName,
|
||||
String opinion) {
|
||||
requireOperator(schId, taskId, operatorId, operatorName);
|
||||
return taskService.handle(schId, taskId, false, opinion, operatorId);
|
||||
}
|
||||
|
||||
private void requireOperator(Long schId, Long taskId, Integer operatorId, String operatorName) {
|
||||
if (schId == null || taskId == null || operatorId == null) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "缺少必要参数:schId/taskId/operatorId");
|
||||
}
|
||||
if (operatorName == null || operatorName.trim().isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "缺少必要参数:operatorName(操作人姓名,审计用)");
|
||||
}
|
||||
}
|
||||
|
||||
private OapiModels.TodoTaskVo toTodoTaskVo(FlwTask t, FlwInstance instance) {
|
||||
OapiModels.TodoTaskVo vo = new OapiModels.TodoTaskVo();
|
||||
vo.setTaskId(t.getId());
|
||||
vo.setInstanceId(t.getInstanceId());
|
||||
vo.setInstanceVersion(instance == null ? null : instance.getVersion());
|
||||
vo.setBizType(instance == null ? null : instance.getBizType());
|
||||
vo.setBizId(instance == null ? null : instance.getBizId());
|
||||
vo.setBizTitle(instance == null ? null : instance.getBizTitle());
|
||||
vo.setBizSnapshot(instance == null ? null : instance.getBizSnapshot());
|
||||
vo.setNodeId(t.getNodeId());
|
||||
vo.setNodeName(t.getNodeName());
|
||||
vo.setAssigneeId(t.getAssigneeId());
|
||||
vo.setAssigneeName(t.getAssigneeName());
|
||||
vo.setArriveTime(t.getArriveTime() == null ? null : t.getArriveTime().toString());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private OapiModels.InstanceItemVo toInstanceItemVo(FlwInstance instance) {
|
||||
OapiModels.InstanceItemVo vo = new OapiModels.InstanceItemVo();
|
||||
vo.setInstanceId(instance.getId());
|
||||
vo.setStatus(instance.getStatus());
|
||||
vo.setVersion(instance.getVersion());
|
||||
vo.setBizType(instance.getBizType());
|
||||
vo.setBizId(instance.getBizId());
|
||||
vo.setBizTitle(instance.getBizTitle());
|
||||
vo.setBizUrl(instance.getBizUrl());
|
||||
vo.setSubmitterName(instance.getSubmitterName());
|
||||
vo.setSubmitTime(instance.getSubmitTime() == null ? null : instance.getSubmitTime().toString());
|
||||
vo.setFinishTime(instance.getFinishTime() == null ? null : instance.getFinishTime().toString());
|
||||
vo.setCurrentNodes(instance.getCurrentNodes());
|
||||
return vo;
|
||||
}
|
||||
|
||||
/** 与 FlowQueryService 同口径(page<1→0;size 空/越界→10) */
|
||||
private static int normPage(Integer page) {
|
||||
return page == null || page < 1 ? 0 : page - 1;
|
||||
}
|
||||
|
||||
private static int normSize(Integer size) {
|
||||
return size == null || size < 1 || size > 100 ? 10 : size;
|
||||
}
|
||||
|
||||
private static <T> BasePageResult<T> toPageResult(long total, List<T> content, Integer page, Integer size) {
|
||||
BasePageResult<T> result = new BasePageResult<>();
|
||||
result.setTotal((int) total);
|
||||
result.setContent(content);
|
||||
int pageIndex = page == null || page < 1 ? 1 : page;
|
||||
int pageSize = size == null || size < 1 ? content.size() : size;
|
||||
result.setPageIndex(pageIndex);
|
||||
result.setPageSize(pageSize);
|
||||
result.setTotalPages(pageSize == 0 ? 0 : (int) Math.ceil((double) total / pageSize));
|
||||
result.setHasNext((long) pageIndex * pageSize < total);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String emptyToNull(String s) {
|
||||
return s == null || s.trim().isEmpty() ? null : s.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回(托管接入件,2026-09-16 补:原先只有 CAS 用户面 {@code /api/flow/instance/{id}/withdraw},
|
||||
* 业务应用只能让浏览器直调,票据与 CORS 一并外溢)。治理口径:
|
||||
|
||||
Reference in New Issue
Block a user