[v2.1] B 审批代理补齐:增 ByBiz/RecordUrl/TaskPageUrl 端点,Approve 透传 edgeName;身份仍全取 CAS 会话前端不可传;补 Controller/Client 单测
build / build-push (push) Successful in 1m17s

This commit is contained in:
chenxing
2026-10-19 16:55:41 +08:00
parent 986a875279
commit 7c5391458c
4 changed files with 452 additions and 2 deletions
@@ -1,7 +1,11 @@
package com.yunhai.notice.controller.api;
import com.yunhai.notice.config.Config;
import com.yunhai.notice.entity.Notice;
import com.yunhai.notice.repository.NoticeRepository;
import com.yunhai.notice.service.flow.NoticeApprovalClient;
import com.yunhai.notice.service.flow.NoticeFlowEmbedService;
import com.yunhai.notice.service.flow.NoticeFlowGuard;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import com.yunhai.platform.utils.model.BoaUser;
@@ -15,15 +19,19 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* 应用内审批(第三步 T-C4):"我的待审核"数据面。前端不直调 oapi——
* operatorId/operatorName 一律取 CAS 登录态(BoaUser),请求参数里不收身份字段,
* 应用内审批(第三步 T-C4):"我的待审核"数据面 + v2.1 业务详情卡片/记录/完整办理换址。
* 前端不直调 oapi——operatorId/operatorName 一律取 CAS 登录态(BoaUser),请求参数里不收身份字段,
* 防伪造办理人;对端 flow-center 再按任务办理人复核(双层)。
* <ul>
* <li>GET Approval/Todo:待办分页(bizType=notice 固定);</li>
* <li>GET Approval/Instances:办件分页(status 可选);</li>
* <li>GET Approval/ByBiz:当前人在该通知上的待办(0/1/N,卡片据此决定可写还是只读);</li>
* <li>GET Approval/RecordUrl:审批记录抽屉地址(token 只读页,复用 {@link NoticeFlowEmbedService});</li>
* <li>GET Approval/TaskPageUrl:完整办理任务页地址(先确认 taskId 属于当前人待办);</li>
* <li>POST Approval/Approve / Reject:办理(body 仅 opinion/edgeName)。</li>
* </ul>
*/
@@ -33,6 +41,9 @@ import java.util.Map;
public class NoticeApprovalController {
private final NoticeApprovalClient approvalClient;
private final NoticeFlowEmbedService embedService;
private final NoticeRepository noticeRepository;
private final NoticeFlowGuard noticeFlowGuard;
/** 我的待审核(列表页数据源) */
@GetMapping("Todo")
@@ -72,6 +83,79 @@ public class NoticeApprovalController {
body == null ? null : body.get("opinion"));
}
/**
* 当前人在该通知上的待办(v2.1 卡片数据源)。身份只取会话;{@code serid} 先过与记录页
* 同一套可见性守卫(未发布通知仅提交人/管理员/办理人/被抄送人可见)。
* <p>出参 {@code tasks} 的 0/1/N 由前端据此决定"只读 / 可办理 / 多个待办引导去列表",
* 本端不猜选、不合并。办件不存在与"有办件但不是我的待办"同形(空 tasks),不用于探测。
*/
@GetMapping("ByBiz")
public Map<String, Object> byBiz(@RequestParam("serid") Integer serid, BoaUser user) {
Operator op = requireOperator(user);
requireVisible(serid, user);
return approvalClient.byBiz(op.schId, op.userId, serid);
}
/**
* 审批记录抽屉地址(token 只读页):直接复用 {@link NoticeFlowEmbedService#embedUrl},
* 那里已做通知存在性 + 未发布可见性 + "尚未进入流程"三重校验,且 token 现取现发不缓存。
* 与 {@code /api/notice/flow/Embed} 同一条实现,避免两个入口对同一 Drawer 漂移出不同守卫口径。
*/
@GetMapping("RecordUrl")
public Map<String, String> recordUrl(@RequestParam("serid") Integer serid, BoaUser user) {
requireOperator(user);
return embedService.embedUrl(serid, user);
}
/**
* 完整办理任务页地址(CAS 会话页,无 token)。
* <p>先打 byBiz 确认 {@code taskId} 确实在<b>当前人</b>该通知的待办里才签发——任务页是写面,
* 顺手签发一个别人的 taskId 等于替他人挂一个办理入口。flow-center 侧仍会按 assigneeId
* 逐字复核,本处只是不把无效地址散播到前端。
*/
@GetMapping("TaskPageUrl")
public Map<String, Object> taskPageUrl(@RequestParam("serid") Integer serid,
@RequestParam("taskId") Long taskId, BoaUser user) {
Operator op = requireOperator(user);
requireVisible(serid, user);
if (!isMyPendingTask(serid, taskId, op)) {
throw new BizException(ErrorCode.FORBIDDEN, "该任务不是你在本通知上的待办");
}
return approvalClient.taskPageUrl(op.schId, taskId);
}
/** byBiz 出参里是否含该 taskId(对端 tasks 元素为 {taskId,nodeId,nodeName,arriveTime,outlets}) */
@SuppressWarnings("unchecked")
private boolean isMyPendingTask(Integer serid, Long taskId, Operator op) {
Map<String, Object> data = approvalClient.byBiz(op.schId, op.userId, serid);
Object tasks = data == null ? null : data.get("tasks");
if (!(tasks instanceof List)) {
return false;
}
for (Object row : (List<Object>) tasks) {
if (row instanceof Map) {
Object id = ((Map<String, Object>) row).get("taskId");
if (id instanceof Number && taskId.equals(((Number) id).longValue())) {
return true;
}
}
}
return false;
}
/**
* 通知可见性守卫(口径与 {@link NoticeFlowEmbedService#embedUrl} 逐字一致):存在性 +
* 未发布(status!=1)走 {@link NoticeFlowGuard}。已发布通知照常放行(全校可见,审批数据面
* 只对办理人出 tasks)。
*/
private void requireVisible(Integer noticeId, BoaUser user) {
Notice notice = noticeRepository.findById(noticeId)
.orElseThrow(() -> new BizException(ErrorCode.FORBIDDEN, "通知不存在: " + noticeId));
if (notice.getStatus() == null || notice.getStatus() != 1) {
noticeFlowGuard.requireUnpublishedVisible(notice, user);
}
}
/** 登录态提参:无身份字段可被请求注入;姓名兜底账号(与 NoticeFlowService 提交人口径一致) */
private static Operator requireOperator(BoaUser user) {
if (user == null || user.getId() == null) {
@@ -72,6 +72,41 @@ public class NoticeApprovalClient {
return openCall.get(TARGET, "/oapi/flow/instances", query, Map.class);
}
/**
* 当前人在本通知上的待办(v2.1 业务详情卡片数据源,{@code /oapi/flow/tasks/by-biz})。
* <p>返回 Map 即对端 {@code ByBizTasksVo} JSON:{@code tasks[]} 0/1/N 三种计数都走 HTTP 200,
* <b>调用方不得据 tasks 反推"这个通知上有没有别人的待办"</b>——办件不存在、错学校、错 bizType、
* 非办理人四种情形出参完全同形(都是空 tasks),这是对端的防探测设计,本端不要试图区分。
* <p>{@code currentNodes} 是节点 <b>id</b> 逗号串(办件原文口径),人读名称取 {@code tasks[].nodeName}。
*/
@SuppressWarnings("unchecked")
public Map<String, Object> byBiz(Integer schId, Integer operatorId, Integer noticeId) {
requireEnabled();
Map<String, String> query = new LinkedHashMap<>();
query.put("schId", String.valueOf(schId));
query.put("bizType", FlowCenterClient.BIZ_TYPE);
query.put("bizId", String.valueOf(noticeId));
query.put("operatorId", String.valueOf(operatorId));
return openCall.get(TARGET, "/oapi/flow/tasks/by-biz", query, Map.class);
}
/**
* 完整办理任务页地址({@code /oapi/flow/embed/url?mode=task}):返回 CAS 会话页
* {@code {front}/#/task/{taskId}},<b>无 token</b>(写面,办理权由该页加载时的用户面接口按
* assigneeId 复核)。仍走 {@link YschOpenCall} 泛化调用——nexus 已发布快照的
* {@code YschFlowOpenApi} 只有 embedUrl/todoTasks/instances,没有 taskPageUrl 门面方法,
* 而本批纪律是不动 BasePlatform。SDK 补齐后可平替(URL 不变,测试锁死)。
*/
@SuppressWarnings("unchecked")
public Map<String, Object> taskPageUrl(Integer schId, Long taskId) {
requireEnabled();
Map<String, String> query = new LinkedHashMap<>();
query.put("schId", String.valueOf(schId));
query.put("mode", "task");
query.put("taskId", String.valueOf(taskId));
return openCall.get(TARGET, "/oapi/flow/embed/url", query, Map.class);
}
/** 同意(opinion 可空;多出口 edgeName 透传) */
public void approve(Long taskId, Integer schId, Integer operatorId, String operatorName,
String opinion, String edgeName) {