Files
ERP/oa-backend/src/main/java/com/kaidi/oa/web/BlogController.java
T
QiufengandClaude Opus 4.8 5e51dc3f56 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>
2026-06-15 19:19:15 +08:00

168 lines
7.5 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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));
}
}