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 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() { return ApiResp.ok(blogRepo.findAll()); } @GetMapping("/{id}") public ApiResp 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 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 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)); } }