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:
Qiufeng
2026-06-15 19:19:15 +08:00
co-authored by Claude Opus 4.8
commit 5e51dc3f56
10584 changed files with 2501339 additions and 0 deletions
@@ -0,0 +1,124 @@
package com.kaidi.oa.web;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.domain.FormInstance;
import com.kaidi.oa.domain.SysUser;
import com.kaidi.oa.repository.FormInstanceRepository;
import com.kaidi.oa.repository.ProjectRepository;
import com.kaidi.oa.service.AuthorizationService;
import com.kaidi.oa.service.WorkflowService;
import com.kaidi.oa.web.AlertController.Alert;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Mobile work-bench aggregation (Request.MD 3.10 移动工作台后端聚合). Folds the
* counts and short feeds the mobile home screen needs into a single response:
* pending tasks, platform alerts, danger-level alerts and project total, plus
* the first five alerts and first five todos. Alert aggregation is reused from
* AlertController to avoid re-implementing the warning logic.
*/
@RestController
@RequestMapping("/api/oa/mobile")
public class MobileController {
/** Maximum number of items in each short preview list. */
private static final int PREVIEW_LIMIT = 5;
private final FormInstanceRepository instanceRepo;
private final ProjectRepository projectRepo;
private final AlertController alertController;
private final CurrentUserResolver currentUser;
private final AuthorizationService authz;
private final WorkflowService workflowService;
public MobileController(FormInstanceRepository instanceRepo,
ProjectRepository projectRepo,
AlertController alertController,
CurrentUserResolver currentUser,
AuthorizationService authz,
WorkflowService workflowService) {
this.instanceRepo = instanceRepo;
this.projectRepo = projectRepo;
this.alertController = alertController;
this.currentUser = currentUser;
this.authz = authz;
this.workflowService = workflowService;
}
@GetMapping("/summary")
public ApiResp<Map<String, Object>> summary(HttpServletRequest http) {
// 平台预警里含证件PII/预算/履约等敏感聚合,与 /alerts 同源——移动端汇总对非 ADMIN/APPROVER
// 一律剥除预警内容与计数,只保留本人待办/项目数,避免移动端成为敏感读门槛的旁路。
SysUser su = currentUser.resolve(http);
Set<String> roles = su == null ? Collections.emptySet() : authz.roleCodesOf(su.getId());
boolean canSeeAlerts = roles.contains("ADMIN") || roles.contains("APPROVER");
// Pending tasks: same todo definition as TaskController (待办 / 办理中 /
// 已退回 form instances)。**按查看者过滤为"本人发起或本人当前办理"的单据**——否则该聚合端点
// 会把全库待办的标题/类别/审批节点端给任意 USER,旁路 form-instance 详情的对象级读门槛(读 IDOR)。
List<FormInstance> todoInstances = new ArrayList<>();
for (FormInstance it : instanceRepo.findAll()) {
String st = it.getStatus();
boolean pending = "待办".equals(st) || "办理中".equals(st) || "已退回".equals(st);
if (!pending) {
continue;
}
boolean mine = authz.isOwner(su, it.getOriginUserId(), it.getOriginUser())
|| workflowService.isCurrentHandler(it.getId(), su);
if (mine) {
todoInstances.add(it);
}
}
// Platform alerts reused from AlertController.aggregate() —— 仅对有权看敏感聚合的角色计算/返回。
List<Alert> alerts = canSeeAlerts ? alertController.aggregate() : Collections.emptyList();
int dangerCount = 0;
for (Alert a : alerts) {
if ("danger".equals(a.level())) {
dangerCount++;
}
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("todoCount", todoInstances.size());
data.put("alertCount", alerts.size());
data.put("dangerCount", dangerCount);
data.put("projectCount", (int) projectRepo.count());
data.put("alerts", preview(alerts));
data.put("todos", todoPreview(todoInstances));
return ApiResp.ok(data);
}
/** First five alerts, untouched. */
private List<Alert> preview(List<Alert> alerts) {
return alerts.size() > PREVIEW_LIMIT
? new ArrayList<>(alerts.subList(0, PREVIEW_LIMIT))
: alerts;
}
/** First five todos projected to a compact map for the mobile list. */
private List<Map<String, Object>> todoPreview(List<FormInstance> todos) {
List<Map<String, Object>> out = new ArrayList<>();
int limit = Math.min(PREVIEW_LIMIT, todos.size());
for (int i = 0; i < limit; i++) {
FormInstance it = todos.get(i);
Map<String, Object> m = new LinkedHashMap<>();
m.put("id", it.getId());
m.put("title", it.getTitle());
m.put("category", it.getCategory());
m.put("status", it.getStatus());
m.put("currentNode", it.getCurrentNode());
out.add(m);
}
return out;
}
}