package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.FlowTrace; import com.kaidi.oa.domain.FormInstance; import com.kaidi.oa.domain.Delegation; import com.kaidi.oa.repository.FlowTraceRepository; import com.kaidi.oa.repository.FormInstanceRepository; import com.kaidi.oa.service.NodeAssigneeResolver; import com.kaidi.oa.service.NotificationService; import com.kaidi.oa.service.WorkflowService; import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.time.Instant; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** * Per-user task views over form instances, mirroring the frontend four-list model. * Identity is taken from the auth token (server-authoritative), NOT from a query param. * - todo: in-flight items whose CURRENT node resolves to me (NodeAssigneeResolver) * - done: 已办结 items I originated or acted on (appear in the flow trace) * - sent: non-draft items I originated * - draft: 草稿 items I originated */ @RestController @RequestMapping("/api/oa/tasks") public class TaskController { private final FormInstanceRepository instanceRepo; private final FlowTraceRepository traceRepo; private final CurrentUserResolver currentUser; private final NodeAssigneeResolver assigneeResolver; private final NotificationService notifications; private final WorkflowService workflowService; private final com.kaidi.oa.service.AuthorizationService authz; public TaskController(FormInstanceRepository instanceRepo, FlowTraceRepository traceRepo, CurrentUserResolver currentUser, NodeAssigneeResolver assigneeResolver, NotificationService notifications, WorkflowService workflowService, com.kaidi.oa.service.AuthorizationService authz) { this.instanceRepo = instanceRepo; this.traceRepo = traceRepo; this.currentUser = currentUser; this.assigneeResolver = assigneeResolver; this.notifications = notifications; this.workflowService = workflowService; this.authz = authz; } @GetMapping public ApiResp>> list( @RequestParam(defaultValue = "todo") String type, HttpServletRequest request) { // Identity is taken from the auth token, not the (now ignored) ?user= param. String me = currentUser.resolveLabel(request); List all = instanceRepo.findAll(); // Principals who have delegated their 待办 to me (active, in-window). When the node // owner is one of these, the item also surfaces in my box (委托代理). Set myPrincipals = principalsDelegatingTo(me); List filtered = switch (type) { case "todo" -> all.stream() .filter(it -> "待办".equals(it.getStatus()) || "办理中".equals(it.getStatus()) || "已退回".equals(it.getStatus())) // A 转交/加签 改派 sets assigneeOverride: that person becomes the sole todo owner // (so the item moves into their box and out of the original heuristic owner's). // Otherwise fall back to the heuristic node→user resolver; a delegate also // sees their principal's items. .filter(it -> { String override = it.getAssigneeOverride(); if (override != null && !override.isBlank()) { return me.equals(override) || myPrincipals.contains(override); } // 真并行审批同步节点 (currentNodeId 以「并行会签@」开头): the synthetic 会签 label // resolves to a real approver, not the submitter, so the heuristic owner alone // would hide the item from the submitter's 待办 box (UI 无法办理). Treat the // submitter AND every distinct group approver as owners; the current user is in // the box if they (or a principal they represent) match any of them. if (workflowService.isParallelSyncNodeId(it.getCurrentNodeId())) { Set owners = workflowService.parallelSyncTodoOwners(it); for (String owner : owners) { if (me.equals(owner) || myPrincipals.contains(owner)) { return true; } } return false; } String owner = assigneeResolver.resolve(it.getCurrentNode(), it.getOriginUser()); return me.equals(owner) || myPrincipals.contains(owner); }) .toList(); case "done" -> { Set involved = instanceIdsIActedOn(me); yield all.stream() .filter(it -> "已办结".equals(it.getStatus())) .filter(it -> me.equals(it.getOriginUser()) || involved.contains(it.getId())) .toList(); } case "sent" -> all.stream() .filter(it -> !"草稿".equals(it.getStatus())) .filter(it -> me.equals(it.getOriginUser())) .toList(); case "draft" -> all.stream() .filter(it -> "草稿".equals(it.getStatus())) .filter(it -> me.equals(it.getOriginUser())) .toList(); default -> throw new ApiException(400, "unknown task type: " + type); }; return ApiResp.ok(filtered.stream().map(TaskController::toView).toList()); } /** * 督办/催办 (M1): bump the instance's urgeCount + lastUrgedAt and push a 督办 notice to * the current node's handler. Persists so a page refresh keeps the 已督办 mark. No-op-safe * on 已办结 items (returns the current counters without notifying). */ @PostMapping("/{instanceId}/urge") public ApiResp> urge(@PathVariable Long instanceId, HttpServletRequest request) { FormInstance inst = instanceRepo.findById(instanceId) .orElseThrow(() -> new NotFoundException("instance not found: " + instanceId)); // 对象级:催办会向当前办理人推送通知,仅发起人本人(按稳定 originUserId)或 ADMIN 可催办自己的单据, // 杜绝任意登录用户枚举 instanceId 对他人单据的办理人刷督办通知(BFLA/通知骚扰)。 com.kaidi.oa.domain.SysUser su = currentUser.resolve(request); if (!authz.isAdmin(su) && !authz.isOwner(su, inst.getOriginUserId(), inst.getOriginUser())) { throw new com.kaidi.oa.common.ApiException(403, "无权督办:仅发起人本人或管理员可催办该事项"); } String me = currentUser.resolveLabel(request); if (!"已办结".equals(inst.getStatus())) { int next = (inst.getUrgeCount() == null ? 0 : inst.getUrgeCount()) + 1; inst.setUrgeCount(next); inst.setLastUrgedAt(Instant.now()); inst.setUpdatedAt(Instant.now()); instanceRepo.save(inst); String handler = inst.getAssigneeOverride() != null && !inst.getAssigneeOverride().isBlank() ? inst.getAssigneeOverride() : assigneeResolver.resolve(inst.getCurrentNode(), inst.getOriginUser()); notifications.notify(handler, "督办", "督办提醒:" + inst.getTitle(), "事项「" + inst.getTitle() + "」在节点「" + inst.getCurrentNode() + "」被 " + me + " 督办(第 " + next + " 次),请尽快办理。", "instance", inst.getId()); } Map out = new LinkedHashMap<>(); out.put("id", inst.getId()); out.put("urgeCount", inst.getUrgeCount() == null ? 0 : inst.getUrgeCount()); out.put("lastUrgedAt", inst.getLastUrgedAt()); return ApiResp.ok(out); } /** Principals who have an active, in-window delegation to me (so I see their 待办). */ private Set principalsDelegatingTo(String me) { return workflowService.principalsDelegatingTo(me); } /** Instance ids where the given user appears as an actor in the flow trace. */ private Set instanceIdsIActedOn(String me) { Set ids = new HashSet<>(); for (FlowTrace t : traceRepo.findAll()) { if (me.equals(t.getWho())) { ids.add(t.getInstanceId()); } } return ids; } private static Map toView(FormInstance inst) { Map m = new LinkedHashMap<>(); m.put("id", inst.getId()); m.put("templateId", inst.getTemplateId()); m.put("templateName", inst.getTemplateName()); m.put("category", inst.getCategory()); m.put("title", inst.getTitle()); m.put("status", inst.getStatus()); m.put("currentNode", inst.getCurrentNode()); m.put("currentNodeId", inst.getCurrentNodeId()); m.put("nodeIndex", inst.getNodeIndex()); m.put("originUser", inst.getOriginUser()); m.put("assigneeOverride", inst.getAssigneeOverride()); m.put("urgeCount", inst.getUrgeCount() == null ? 0 : inst.getUrgeCount()); m.put("lastUrgedAt", inst.getLastUrgedAt()); m.put("createdAt", inst.getCreatedAt()); m.put("updatedAt", inst.getUpdatedAt()); return m; } }