SNAPSHOT W7 已部署稳定态 — 凯迪ERP+OA一体化平台 (MET 73.3%)
恢复点(restore point)。别人改崩后可 git reset --hard 回到此提交。 == 此快照内容 == - 后端 oa-backend: 734 控制器 / 711 实体 (Spring Boot 3.2.5 + SQLite, 端口8091) - 前端 modern-ui/app: Vue3+Vite, 约700页 (构建产物已在 oa-backend/src/main/resources/static) - 数据库 oa-backend/data/oa.db: 含全部演示数据 (强制入库, 6.6MB) - 交接文档 go.md + go-code-reference/endpoints/entities/database.md - 多代理建设脚本 .claude/wf-*.js == 状态 == - 对 凯迪科技ERP_20260507.xlsx 合规 MET ~73.3% (PARTIAL 75: 34可建+6种子/bug+35外部硬天花板) - 安全: 5轮红队+5轮复检, default-deny分级鉴权, 连续零可利用 - W3~W7 累计补完436缺口; W8末轮(40缺口)为半成品(源码树可编译但未集成) - 运行: cd oa-backend; java -jar build/libs/oa-backend-0.1.0.jar --server.port=8091; admin/123456 == 排除(gitignore, 可再生) == node_modules / oa-backend/build / .jdks / *.log / Backup-ERP-* / 弃用的OFBiz核心(只保留modern-ui) 完整文件夹备份见同目录 Backup-ERP-20260615-191517/ (含上述全部, 仅缺 node_modules) 时间戳: 20260615-191517 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
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<Map<String, Object>>> 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<FormInstance> 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<String> myPrincipals = principalsDelegatingTo(me);
|
||||
|
||||
List<FormInstance> 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<String> 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<Long> 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<Map<String, Object>> 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<String, Object> 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<String> principalsDelegatingTo(String me) {
|
||||
return workflowService.principalsDelegatingTo(me);
|
||||
}
|
||||
|
||||
/** Instance ids where the given user appears as an actor in the flow trace. */
|
||||
private Set<Long> instanceIdsIActedOn(String me) {
|
||||
Set<Long> ids = new HashSet<>();
|
||||
for (FlowTrace t : traceRepo.findAll()) {
|
||||
if (me.equals(t.getWho())) {
|
||||
ids.add(t.getInstanceId());
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static Map<String, Object> toView(FormInstance inst) {
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user