From b07394eed2aa0cca8c3e9470d445dd128de44f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=BB=AB?= <486494914@qq.com> Date: Mon, 19 Oct 2026 16:11:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=90=8E=E5=8F=B0=E5=88=A0?= =?UTF-8?q?=E4=B8=8D=E6=8E=89=E5=86=85=E5=AE=B9=E7=B3=BB=E7=BB=9F=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E7=9A=84=E6=96=87=E7=AB=A0=EF=BC=8C=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E9=87=8D=E5=90=AF=E5=90=8E=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除文章:先删引用它的发布记录和实体关联再删文章(之前顺序反了,数据库外键拒绝删除),并放在一个事务里,失败时不会只删掉一半 - 管理员登录状态存到数据库(admin_sessions,只存令牌哈希),服务重启或部署后不用重新登录;登录过期时后台回到登录页并提示,而不是操作一直失败 - 内容系统发布的文章可修改知识中心列表里的摘要 - 文章缺少扩展记录时,修改分类和发布时间也能保存 - 提示文字改为“约 1 分钟内在网站生效”(前台页面缓存 60 秒) - 新增不连数据库的单元测试 tests/adminFixes.unit.test.js(其他测试会写 .env 里的库) Co-Authored-By: Claude Opus 5.5 --- frontend/src/app/admin/ArticleManager.tsx | 18 ++--- frontend/src/app/admin/page.tsx | 4 +- src/database/migrate.js | 12 ++++ src/models/Article.js | 1 + src/routes/adminRoutes.js | 11 ++- src/services/AdminAuthService.js | 27 ++++---- src/services/ContentService.js | 19 ++++-- tests/adminFixes.unit.test.js | 81 +++++++++++++++++++++++ 8 files changed, 145 insertions(+), 28 deletions(-) create mode 100644 tests/adminFixes.unit.test.js diff --git a/frontend/src/app/admin/ArticleManager.tsx b/frontend/src/app/admin/ArticleManager.tsx index 9f8cd61..62dd69a 100644 --- a/frontend/src/app/admin/ArticleManager.tsx +++ b/frontend/src/app/admin/ArticleManager.tsx @@ -5,9 +5,9 @@ import { FormEvent, useEffect, useMemo, useState } from 'react'; type Row = { content_id: string; title: string; publish_status: string; created_at: string; updated_at: string; category?: string | null; tags?: string | null; author?: string | null; published_at?: string | null; - source: 'content_system' | 'manual'; public_path: string; + source: 'content_system' | 'manual'; public_path: string; summary?: string | null; }; -type Draft = { content_id: string; title: string; body: string; publish_status: string; category: string; tags: string; author: string; published_at: string; source?: Row['source'] }; +type Draft = { content_id: string; title: string; body: string; publish_status: string; category: string; tags: string; author: string; published_at: string; summary: string; source?: Row['source'] }; type Request = (path: string, options?: RequestInit) => Promise; const STATUS: Record = { published: '已发布', draft: '草稿', review: '审核中', unpublished: '已下线', historical: '历史版本' }; @@ -26,7 +26,7 @@ function toText(value?: string | null) { const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } -const empty = (): Draft => ({ content_id: '', title: '', body: '', publish_status: 'draft', category: '', tags: '', author: '阿美巴', published_at: '' }); +const empty = (): Draft => ({ content_id: '', title: '', body: '', publish_status: 'draft', category: '', tags: '', author: '阿美巴', published_at: '', summary: '' }); /** Articles: search, filter, publish or take offline, edit, and delete one or many. */ export function ArticleManager({ request, onMessage, onError }: { request: Request; onMessage: (text: string) => void; onError: (text: string) => void }) { @@ -72,23 +72,24 @@ export function ArticleManager({ request, onMessage, onError }: { request: Reque function toggle(row: Row) { const next = row.publish_status === 'published' ? 'unpublished' : 'published'; void run(() => request(`/api/admin/contents/article/${row.content_id}`, { method: 'PUT', body: JSON.stringify({ publish_status: next }) }), - next === 'published' ? '文章已上线。' : '文章已下线,网站上不再显示。'); + next === 'published' ? '文章已上线,网站约 1 分钟内显示。' : '文章已下线,网站约 1 分钟内不再显示。'); } async function edit(row: Row) { try { const full = await request(`/api/admin/contents/article/${row.content_id}`); setEditing({ content_id: row.content_id, title: full.title || '', body: full.body || '', publish_status: full.publish_status || 'draft', - category: full.category || '', tags: full.tags || '', author: full.author || '', published_at: toInput(full.published_at), source: row.source }); + category: full.category || '', tags: full.tags || '', author: full.author || '', published_at: toInput(full.published_at), summary: row.summary || '', source: row.source }); } catch (e: any) { onError(e.message); } } async function save(event: FormEvent) { event.preventDefault(); if (!editing) return; const data = { title: editing.title.trim(), body: editing.body, publish_status: editing.publish_status, category: editing.category, - tags: editing.tags, author: editing.author, published_at: editing.published_at ? `${editing.published_at.replace('T', ' ')}:00` : '' }; + tags: editing.tags, author: editing.author, published_at: editing.published_at ? `${editing.published_at.replace('T', ' ')}:00` : '', + ...(editing.source === 'content_system' ? { summary: editing.summary } : {}) }; const path = editing.content_id ? `/api/admin/contents/article/${editing.content_id}` : '/api/admin/contents/article'; await run(async () => { await request(path, { method: editing.content_id ? 'PUT' : 'POST', body: JSON.stringify(data) }); setEditing(null); }, - editing.content_id ? '修改已保存,网站上立即生效。' : '文章已新增。'); + editing.content_id ? '修改已保存,网站约 1 分钟内更新。' : '文章已新增。'); } return
@@ -134,8 +135,9 @@ export function ArticleManager({ request, onMessage, onError }: { request: Reque {editing &&
{ if (e.target === e.currentTarget) setEditing(null); }}>

{editing.content_id ? '编辑文章' : '新增文章'}

- {editing.source === 'content_system' &&

这篇文章来自内容系统。这里的修改会立即生效,但内容系统再次发布同一篇文章时会覆盖这里的修改。

} + {editing.source === 'content_system' &&

这篇文章来自内容系统。这里的修改约 1 分钟内在网站生效,但内容系统再次发布同一篇文章时会覆盖这里的修改。

} + {editing.source === 'content_system' &&