feat: 实现内容系统 MVC(Post CRUD + 审核流转)

按 content-system.md 设计文档,实现帖子内容系统的最小可行 MVC。

新增:
- Model: Post 模型,含 5 种状态(draft/pending/approved/rejected/locked)
- Repository: post_repo.go,分页查询(前台 approved 列表 + 后台全状态筛选)
- Service: post_service.go,核心业务逻辑(状态流转、Markdown 渲染 Goldmark)
- Controller: post_controller + admin_post_controller,SSR 页面 6 个 + API 13 个
- Template: 列表/详情/编辑器/404 + 管理员列表,各配 CSS/JS
- 路由: /posts, /posts/:id, /posts/new, /posts/:id/edit, REST API, Admin API
- gomod: + github.com/yuin/goldmark v1.8.2

修改:
- Main: AutoMigrate Post 表
- Router: deps 注册 PostService/Controller,路由注册
- Nav: 新增“帖子”导航链接
- Admin Sidebar: 新增“内容管理”入口

设计:
- 审核开关复用 audit.enabled,开启时帖子为 draft,关闭后直接 approved
- 仅 approved 公开可见,作者/admin 可预览非公开状态帖子
- 严格分层 ISP 接口: Controller → postUseCase, Service → postStore, Repo → *PostRepo
- 状态保护: pending/locked 禁止编辑,rejected 编辑后自动重置为 draft
This commit is contained in:
2026-05-27 18:22:48 +08:00
parent fc65872882
commit 03a01a09be
26 changed files with 1803 additions and 3 deletions

View File

@ -0,0 +1,85 @@
(function() {
var form = document.getElementById('postForm');
if (!form) return;
var postId = document.getElementById('postId');
var titleEl = document.getElementById('postTitle');
var bodyEl = document.getElementById('postBody');
var previewBtn = document.getElementById('previewBtn');
var previewArea = document.getElementById('previewArea');
var previewContent = document.getElementById('previewContent');
var isEdit = postId && postId.value;
// 获取 CSRF token
function getCSRFToken() {
var meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
}
// 预览
if (previewBtn) {
previewBtn.addEventListener('click', function() {
var body = bodyEl.value.trim();
if (!body) { alert('请输入正文'); return; }
fetch('/api/posts/preview', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken()
},
body: JSON.stringify({ body: body })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
previewContent.innerHTML = data.data.html;
previewArea.style.display = 'block';
} else {
alert(data.message || '预览失败');
}
})
.catch(function() { alert('预览请求失败'); });
});
}
// 提交
form.addEventListener('submit', function(e) {
e.preventDefault();
var title = titleEl.value.trim();
var body = bodyEl.value.trim();
if (!title) { alert('请输入标题'); return; }
if (!body) { alert('请输入正文'); return; }
var url = '/api/posts';
var method = 'POST';
if (isEdit) {
url = '/api/posts/' + postId.value;
method = 'PUT';
}
fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken()
},
body: JSON.stringify({ title: title, body: body })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
if (data.data && data.data.id) {
window.location.href = '/posts/' + data.data.id;
} else {
window.location.href = '/posts';
}
} else {
alert(data.message || '操作失败');
}
})
.catch(function() { alert('请求失败'); });
});
})();