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,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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user