News System 可直接开工实施计划(PostgreSQL + Next.js App Router)

1. 摘要

目标是在现有最简新闻站基础上,落地可运营的后台发布系统:管理员登录、新闻草稿/发布、分类管理(首版仅分类)、前台新闻列表与详情、统一 API 返回结构、完整验收与测试闭环。
实现方式固定为:PostgreSQL、JWT + HttpOnly Cookie、单管理员账号、富文本正文、不做封面上传、API 统一 {code,message,data}。

2. 里程碑与交付顺序

  1. 基础改造:目录迁移到 src/、公共工具层、环境变量与错误模型。
  2. 数据库:建表与索引、初始化管理员、初始化默认分类、JSON 数据迁移脚本。
  3. 鉴权:登录/登出 API、JWT 签发校验、middleware 路由保护。
  4. 前台 API + 页面:新闻列表、详情、仅展示 published。
  5. 后台 API + 页面:新闻管理列表、创建、编辑、发布状态切换。
  6. 联调与测试:接口契约测试、页面关键流程测试、验收清单逐项通过。
  7. 上线准备:生产环境变量、一次性迁移执行、回滚预案。

3. 目标目录结构(落地版)

news-system/ ├── src/ │ ├── app/ │ │ ├── api/ │ │ │ ├── auth/ │ │ │ │ ├── login/route.js │ │ │ │ └── logout/route.js │ │ │ ├── news/ │ │ │ │ ├── route.js │ │ │ │ └── [id]/route.js │ │ │ ├── admin/ │ │ │ │ └── news/ │ │ │ │ ├── route.js │ │ │ │ └── [id]/route.js │ │ ├── admin/ │ │ │ ├── layout.jsx │ │ │ ├── login/page.jsx │ │ │ ├── dashboard/page.jsx │ │ │ └── news/ │ │ │ ├── create/page.jsx │ │ │ └── edit/[id]/page.jsx │ │ ├── news/[id]/page.jsx │ │ ├── layout.jsx │ │ ├── page.jsx │ │ ├── globals.css │ │ └── middleware.js │ ├── lib/ │ │ ├── db.js │ │ ├── auth.js │ │ ├── response.js │ │ ├── validators.js │ │ └── newsService.js │ ├── components/ │ │ ├── Header.jsx │ │ ├── Footer.jsx │ │ ├── NewsCard.jsx │ │ └── RichTextEditor.jsx │ └── scripts/ │ ├── migrate-json-to-pg.js │ └── seed-admin.js ├── .env.local ├── package.json └── tailwind.config.js

4. 数据库表结构(PostgreSQL)

create table admins ( id bigserial primary key, username varchar(50) not null unique, password_hash varchar(255) not null, token_version int not null default 1, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table news_categories ( id bigserial primary key, name varchar(50) not null unique, slug varchar(60) not null unique, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table news ( id bigserial primary key, title varchar(200) not null, summary varchar(500) not null, content_html text not null, content_text text not null, status varchar(20) not null check (status in ('draft','published')), category_id bigint not null references news_categories(id), published_at timestamptz null, created_by bigint not null references admins(id), updated_by bigint not null references admins(id), created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create index idx_news_status_published_at on news(status, published_at desc); create index idx_news_category_status on news(category_id, status, published_at desc); create index idx_news_created_at on news(created_at desc);

初始化规则:

  • news_categories 默认插入 未分类(slug=uncategorized)。
  • seed-admin.js 创建单管理员(来自 .env.local:ADMIN_USERNAME、ADMIN_PASSWORD)。
  • 旧 data/news.json 通过 migrate-json-to-pg.js 一次性导入为 published,published_at=created_at。

5. 环境变量与安全基线

必须项:

  • DATABASE_URL
  • JWT_SECRET
  • ADMIN_USERNAME
  • ADMIN_PASSWORD
  • COOKIE_NAME=ns_admin_token
  • JWT_EXPIRES_IN=7d

安全约束:

  • 登录成功后仅写 HttpOnly + Secure(生产) + SameSite=Lax cookie。
  • middleware 保护 /admin/:path* 与 /api/admin/:path*。
  • 密码哈希 bcrypt。
  • 富文本正文服务端净化(白名单标签),同时保留 content_text 供摘要与检索。

6. API 契约(统一响应)

统一响应:

  • 成功:{ code: 0, message: “ok”, data: … }
  • 失败:{ code: <业务码>, message: “<错误信息>”, data: null }

业务码约定:

  • 0 成功
  • 1001 参数错误
  • 1002 未认证
  • 1003 无权限
  • 1004 资源不存在
  • 1005 状态非法
  • 1500 服务端错误

接口清单:

  1. POST /api/auth/login
    • 入参:{ username, password }
    • 成功:写入 cookie,返回管理员基础信息
    • 失败:1002
  2. POST /api/auth/logout
    • 行为:清空 cookie
    • 成功:code=0
  3. GET /api/news?page=1&pageSize=10&categoryId=&q=
    • 说明:仅返回 published
    • 返回:分页列表 + total
  4. GET /api/news/:id
    • 说明:仅可读取 published
    • 不存在或未发布:1004
  5. GET /api/admin/news?page=1&pageSize=10&status=&categoryId=&q=
    • 说明:后台列表,支持 draft/published 过滤
  6. POST /api/admin/news
    • 入参:{ title, summary, contentHtml, categoryId, status }
    • 规则:status=published 时写 published_at=now()(若首次发布)
  7. GET /api/admin/news/:id
    • 说明:后台详情(可读草稿)
  8. PUT /api/admin/news/:id
    • 入参同创建
    • 规则:draft -> published 时补 published_at;published -> draft 时保留历史 published_at 但前台不展示
  9. DELETE /api/admin/news/:id
    • 首版策略:硬删除
    • 返回:删除结果

7. 页面交互规格

  1. / 首页
    • 展示已发布新闻卡片流,按 published_at desc。
    • 支持分类筛选与关键词搜索(标题/摘要)。
    • 分页导航(默认 10 条)。
  2. /news/[id]
    • 仅展示已发布新闻详情。
    • 显示标题、分类、发布时间、富文本正文。
  3. /admin/login
    • 用户名密码登录。
    • 成功跳转 /admin/dashboard。
    • 已登录访问此页直接跳转 dashboard。
  4. /admin/dashboard
    • 显示统计卡片:总新闻、已发布、草稿。
    • 最近新闻快捷入口。
  5. /admin/news/create
    • 表单字段:标题、摘要、分类、富文本正文、状态(草稿/发布)。
    • 保存后跳转编辑页并提示成功。
  6. /admin/news/edit/[id]
    • 加载详情并可编辑全部字段。
    • 支持“保存草稿”“发布更新”“删除”。
  7. admin/layout
    • 统一侧栏、顶部登出、登录态校验。
    • 所有后台页面复用该布局。

8. 服务层与模块职责

  • lib/db.js:Postgres 连接池与事务封装。
  • lib/auth.js:JWT 签发/验签、cookie 读写、管理员校验。
  • lib/validators.js:zod 请求校验 schema。
  • lib/newsService.js:新闻增删改查与状态流转规则。
  • lib/response.js:统一响应与错误抛出。

约束:

  • Route Handler 不直接写 SQL。
  • 所有写操作走 service 层并统一事务边界。

9. 测试用例与场景

接口测试:

  1. 登录成功/失败、登出清 cookie。
  2. 未登录访问 /api/admin/news 返回 1002。
  3. 创建草稿成功,列表可见,前台不可见。
  4. 草稿发布后前台可见,排序正确。
  5. 已发布改回草稿后前台不可见。
  6. 分类筛选与关键词搜索结果正确。
  7. 非法参数触发 1001。
  8. 删除新闻后后台/前台均不可访问。

页面流程测试:

  1. 管理员登录 -> 进入 dashboard。
  2. 创建新闻(草稿)-> 编辑 -> 发布 -> 前台可浏览。
  3. 未登录访问 /admin/news/create 被重定向到 /admin/login。
  4. 详情页访问不存在 id 显示 404。

回归测试:

  1. 旧 JSON 导入后首页可见历史新闻。
  2. 样式与基础导航不回退。

10. 验收清单(Definition of Done)

  1. 数据库三张核心表已建、索引生效、默认分类与管理员已初始化。
  2. 旧 data/news.json 成功迁移,条数一致。
  3. 所有 API 均按 {code,message,data} 返回并覆盖错误码。
  4. 后台受 middleware + cookie 双层保护。
  5. 新闻支持草稿/发布,前台仅展示已发布。
  6. 分类功能在前后台均可使用(至少筛选)。
  7. 关键流程测试通过,无阻塞级缺陷。
  8. 生产环境变量齐全,可一键启动。

11. 假设与默认值(已锁定)

  • 数据库:PostgreSQL。
  • 鉴权:JWT + HttpOnly Cookie。
  • 账号:单管理员。
  • 正文:富文本编辑。
  • 封面图:首版不做上传。
  • 分类:首版做“仅分类”,不做标签。
  • API 风格:统一 {code,message,data}。
  • 删除策略:首版硬删除(后续可升级软删除)。



落实这套实施计划,不用额外创建 agent 文件、.md、.skill 文件也可以直接开工。
这些文件是给“流程规范/复用能力”用的,不是业务功能必需品。

只有两种情况才建议加:

  1. 你想把这套实施流程沉淀成团队规范(可加 IMPLEMENTATION_PLAN.md)。
  2. 你希望以后复用成自动化能力(再做 SKILL.md)。

就当前目标(把新闻系统做出来),直接按计划改代码、建表、写接口即可。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部