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,167 @@
package com.kaidi.oa.web;
import com.kaidi.oa.common.ApiException;
import com.kaidi.oa.common.ApiResp;
import com.kaidi.oa.common.HtmlSanitizer;
import com.kaidi.oa.common.NotFoundException;
import com.kaidi.oa.domain.Blog;
import com.kaidi.oa.domain.SysUser;
import com.kaidi.oa.repository.BlogRepository;
import com.kaidi.oa.service.AuthorizationService;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.util.List;
/**
* 我的博客 (知识社区). Self-seeds 12 demo posts on first run (when the table is
* empty) so the page stays populated; create/update persist real posts.
*/
@RestController
@RequestMapping("/api/oa/blogs")
public class BlogController {
private final BlogRepository blogRepo;
private final CurrentUserResolver currentUser;
private final AuthorizationService authz;
public BlogController(BlogRepository blogRepo, CurrentUserResolver currentUser,
AuthorizationService authz) {
this.blogRepo = blogRepo;
this.currentUser = currentUser;
this.authz = authz;
}
/**
* 对象级写授权:仅博客作者(按稳定 authorId,空回退 author 名)本人或 ADMIN 可改/下架,
* 杜绝任意登录用户按 id 枚举篡改/下架他人博客(BOLA/IDOR)。
*/
private void assertAuthorOrAdmin(Blog b, HttpServletRequest http) {
SysUser su = currentUser.resolve(http);
if (!authz.isAdmin(su) && !authz.isOwner(su, b.getAuthorId(), b.getAuthor())) {
throw new ApiException(403, "无权操作:仅博客作者本人或管理员可修改/下架该博客");
}
}
private static final String[] PEOPLE = {
"张伟", "李娜", "王芳", "刘洋", "陈静", "赵强", "杨敏", "黄磊", "周婷", "吴昊"
};
private static final String[] CATEGORIES = {
"工作随笔", "技术分享", "项目复盘", "读书笔记", "管理心得"
};
private static final String[] STATUS = {"已发布", "草稿", "审核中", "已下架"};
private static final String[] TITLES = {
"从一次线上故障谈系统稳定性建设",
"OA流程引擎设计的几点思考",
"我在工程部的第一个季度复盘",
"读《卓有成效的管理者》有感",
"如何写一份让人愿意读的会议纪要",
"团队协作中的沟通成本与解法",
"一次跨部门项目的协同实践",
"数据可视化让汇报更有说服力",
"远程办公半年的得与失",
"新人快速融入团队的五个建议",
"低代码平台落地的踩坑记录",
"关于知识沉淀这件小事"
};
private static final String[] TAG_POOL = {
"稳定性", "架构", "复盘", "管理", "协作", "效率", "成长", "工具", "方法论", "随笔"
};
private static <T> T pick(T[] arr, int i) {
return arr[((i % arr.length) + arr.length) % arr.length];
}
@PostConstruct
void seedIfEmpty() {
if (blogRepo.count() > 0) {
return;
}
LocalDate base = LocalDate.of(2026, 6, 10);
for (int i = 0; i < 12; i++) {
Blog b = new Blog();
b.setTitle(pick(TITLES, i));
b.setSummary("记录工作与学习中的思考与实践,希望对同事们有所帮助。本文从实际案例出发,梳理了相关方法与心得,欢迎在评论区交流。");
b.setBody("在实际工作中,我们经常会遇到各种各样的问题。本文结合具体案例,分享我的一些思考与做法。\n一、背景与问题。二、分析与思路。三、落地与效果。四、复盘与改进。\n欢迎大家在评论区交流,也欢迎收藏本文以备查阅。");
b.setAuthor(pick(PEOPLE, i));
b.setCategory(pick(CATEGORIES, i));
b.setStatusId(pick(STATUS, i));
b.setDate(base.minusDays(i * 2L).toString());
b.setViews(320 - i * 18);
b.setComments(24 - i);
b.setLikes(86 - i * 5);
b.setCover(i % 3 == 0);
b.setTags(pick(TAG_POOL, i) + "," + pick(TAG_POOL, i + 4));
blogRepo.save(b);
}
}
@GetMapping
public ApiResp<List<Blog>> list() {
return ApiResp.ok(blogRepo.findAll());
}
@GetMapping("/{id}")
public ApiResp<Blog> get(@PathVariable Long id) {
return ApiResp.ok(blogRepo.findById(id)
.orElseThrow(() -> new NotFoundException("blog not found: " + id)));
}
public record CreateBlogRequest(
String title, String summary, String body, String author,
String category, String statusId, String tags) {
}
@PostMapping
public ApiResp<Blog> create(@RequestBody CreateBlogRequest req, HttpServletRequest http) {
if (req.title() == null || req.title().isBlank()) {
throw new ApiException(400, "title is required");
}
Blog b = new Blog();
b.setTitle(req.title().trim());
b.setSummary(HtmlSanitizer.sanitize(req.summary()));
b.setBody(HtmlSanitizer.sanitize(req.body()));
// 作者署名服务端定调:已登录者强制记为本人(authorId=稳定 userId + author=displayName 展示)
// 未登录回退前端 author(demo 兼容),杜绝冒充他人为作者(批量赋值)。
SysUser su = currentUser.resolve(http);
String label = currentUser.resolveLabel(http);
boolean anon = CurrentUserResolver.DEMO_USER_LABEL.equals(label);
b.setAuthor(anon ? (req.author() == null || req.author().isBlank() ? "张伟" : req.author()) : label);
b.setAuthorId(su != null ? su.getId() : null);
b.setCategory(req.category() == null ? "工作随笔" : req.category());
b.setStatusId(req.statusId() == null ? "已发布" : req.statusId());
b.setTags(req.tags() == null ? "" : req.tags());
b.setDate(LocalDate.of(2026, 6, 11).toString());
b.setViews(0);
b.setComments(0);
b.setLikes(0);
b.setCover(false);
return ApiResp.ok(blogRepo.save(b));
}
public record UpdateBlogRequest(String title, String summary, String body, String category, String statusId, String tags) {
}
@PatchMapping("/{id}")
public ApiResp<Blog> update(@PathVariable Long id, @RequestBody UpdateBlogRequest req,
HttpServletRequest http) {
Blog b = blogRepo.findById(id)
.orElseThrow(() -> new NotFoundException("blog not found: " + id));
assertAuthorOrAdmin(b, http);
if (req.title() != null) b.setTitle(req.title());
if (req.summary() != null) b.setSummary(HtmlSanitizer.sanitize(req.summary()));
if (req.body() != null) b.setBody(HtmlSanitizer.sanitize(req.body()));
if (req.category() != null) b.setCategory(req.category());
if (req.statusId() != null) b.setStatusId(req.statusId());
if (req.tags() != null) b.setTags(req.tags());
return ApiResp.ok(blogRepo.save(b));
}
}