Files
jiang13-bbs/README.md
freefire d784b0ea7a feat: 交付官方 Docker 运行时,外观改背景图并下线自定义 CSS/JS
站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 05:27:33 +08:00

260 lines
10 KiB
Markdown
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.
# 姜十三论坛(jiang13-bbs)
一个基于 Go + Next.js 的现代社区论坛系统,采用 SSR 直出 + HttpOnly Cookie 认证方案,具备完整的帖子/评论/通知/签到/管理后台功能。
> **许可声明:** 本项目为专有软件(Proprietary),源码可见但不构成开源授权。详见 [LICENSE](./LICENSE)。未经书面许可,不得使用、部署或分发。
---
## 技术栈
| 层级 | 技术 |
|------|------|
| 后端 | Go 1.27 · Gin · GORM · PostgreSQL 17 |
| 前端 | Next.js 16 (App Router) · TypeScript · Tailwind CSS v4 |
| 认证 | JWT (HttpOnly Cookie) + Refresh Token 轮转 + CSRF 双提交 + Token Version 撤销 |
| 部署 | 官方:`deploy/` Docker Compose(Next SSR + Go + Postgres,反代自备);本地开发仅 Compose 起数据库 |
---
## 功能概览
### 用户
- 注册 / 登录 / 登出(注册即登录,免二次跳转)
- 个人资料编辑:昵称、头像、邮箱、个性签名
- 修改密码(改密后全端踢下线)
- 个人主页:发帖列表、评论列表(分页)、签到积分
- 每日签到(积分 + 连续签到天数)
### 内容
- 帖子发帖 / 编辑 / 删除(作者本人或管理员)
- 帖子类型:普通帖 / 问答帖
- 帖子置顶 / 推荐(仅管理员)
- 评论楼层制:主评论 = 楼层,子评论不占楼层,最多 10 层嵌套
- 楼层分页 + 内存拼装评论树(单次查询,无 N+1)
- Markdown 渲染(代码高亮、引用、列表等)
- 帖子点赞(联合唯一索引防重复)
### 通知
- 站内通知:评论你的帖子、回复你的评论、点赞你的帖子
- 未读红点 SSR 直出,实时靠 WebSocket 推送,回前台时 HTTP 校准
- 单条标记已读 / 全部已读
### 管理后台
- 公告管理(草稿 / 发布、标签预设色)
- 仪表盘
### SEO
- 全站 SSR/ISR,SEO 关键内容在初始 HTML 中
- 帖子详情页输出 `DiscussionForumPosting` 类型 JSON-LD
- 动态 `sitemap.ts` + `robots.ts`(由 Next.js 对外提供,后端不重复挂 `/sitemap.xml` / `/robots.txt`)
### 安全
- CSP / HSTS / X-Frame-Options: DENY / X-Content-Type-Options / Referrer-Policy
- 速率限制:登录 20/min、注册 10/min、发帖新用户 24h 冷静期
- Refresh Token 一次性轮转 + 盗用检测(吊销后重放 → 杀全家族)
- 生产 HTTPS 启用 `__Host-` Cookie 前缀
---
## 项目结构
```
jiang13-bbs/
├── backend/ # Go 后端
│ ├── cmd/jiang13/ # 入口 main.go
│ ├── config/ # 配置解析(app.ini + 环境变量)
│ ├── handler/ # HTTP 处理器
│ ├── middleware/ # 认证 / CSRF / 限流 / 安全头
│ ├── model/ # GORM 模型 + 数据库初始化
│ ├── router/ # 路由注册
│ ├── service/ # 业务逻辑层
│ ├── app.ini.example # 配置模板
│ └── go.mod
├── frontend/ # Next.js 前端
│ ├── app/ # App Router 页面
│ │ ├── settings/ # 账号设置(资料 + 密码)
│ │ ├── u/[id]/ # 用户主页
│ │ ├── post/[id]/ # 帖子详情
│ │ ├── compose/ # 发帖
│ │ ├── notifications/ # 通知列表
│ │ ├── admin/ # 管理后台
│ │ └── ...
│ ├── components/ # 复用组件
│ ├── lib/ # API 封装 / 工具函数
│ ├── middleware.ts # SSR 登录态静默轮转
│ ├── next.config.ts # Rewrite 代理 / 图片缓存
│ └── .env.example
├── deploy/ # 官方运行时(web + api + Postgres,反代自备)
├── docker-compose.yml # 仅本地开发 Postgres
├── LICENSE # 专有软件许可协议
└── .gitignore
```
---
## 快速开始
### 前置条件
- Go 1.27+
- Node.js 20+ / pnpm 或 npm
- PostgreSQL 17(或通过 Docker Compose 启动)
### 1. 启动数据库
```bash
docker compose up -d postgres
```
### 2. 启动后端
```bash
cd backend
cp app.ini.example app.ini
# 按需修改 app.ini 中的 DSN / JWT_SECRET / DEV_MODE
go mod download
go run ./cmd/jiang13
```
**工作目录必须是 `backend/`。** 上传文件与 JWT 密钥落在 `backend/data/`(头像 `backend/data/uploads/avatars/`)。不要在仓库根或 `backend/cmd/jiang13/` 执行 `go run`,否则会另建一套 data,数据库里头像 URL 会对不上文件。启动日志应含 `workPath=.../backend` 和 `dataDir=.../backend/data`。需要覆盖时设置 `JIANG13_WORK_PATH` 指向 `backend/`。
后端默认监听 `:3001`,首次启动自动执行数据库迁移并写入默认板块。同一时间只跑一个后端进程。
### 3. 启动前端
```bash
cd frontend
cp .env.example .env.local
npm install
npm run dev
```
前端默认监听 `:3000`,通过 `next.config.ts` rewrite 将 `/api/*` 代理到后端。
---
## 配置说明
### 后端(`backend/app.ini`)
```ini
[server]
HTTP_PORT = 3001
[database]
DSN = postgres://postgres:postgres@localhost:5432/jiang13?sslmode=disable
[security]
; 留空则自动生成并持久化到 data/.jwt_secret
; 生产环境务必显式指定强随机值
JWT_SECRET =
[paths]
DATA = data
[app]
DEV_MODE = true
```
环境变量优先级高于 app.ini:`HTTP_PORT`、`DB_DSN`、`JWT_SECRET`、`DEV_MODE`。`JIANG13_WORK_PATH` 可强制指定后端根目录(`app.ini` 与 `data/` 所在处)。
### 前端(`frontend/.env.local`)
```bash
# SSR 与 middleware 直连后端(本地开发)
NEXT_PUBLIC_API_URL=http://localhost:3001
# 部署到 Cloudflare Workers 等边缘环境时必须显式配置
# BACKEND_URL=https://your-backend.example.com
```
---
## 认证架构
采用三 Cookie + Token Version 方案,不使用 localStorage 存储 token:
| Cookie | 用途 | 属性 |
|--------|------|------|
| `j13_token` | Access JWT(15 分钟) | HttpOnly · SameSite=Lax |
| `j13_refresh` | Refresh Token(7 天,DB 存储 SHA-256) | HttpOnly · SameSite=Lax · Path=/ |
| `j13_csrf` | CSRF Token(JS 可读) | SameSite=Lax |
- **SSR 直出登录态:** `middleware.ts` 在页面/RSC 请求前本地解码 access token 的 exp,过期时静默调后端 refresh 轮转,新 cookie 注入请求头供 SSR 使用并透传浏览器。
- **客户端续期:** `lib/api.ts` 的 `fetchWithRefresh` 遇到 401 自动调 `/api/auth/refresh` 重试。
- **撤销机制:** 改密码 / 封禁 / 管理员强制下线时递增 `User.TokenVersion`,所有旧 JWT 立即失效。
- **盗用检测:** Refresh Token 一次性轮转,已吊销 token 重放时撤销该用户全部 refresh token。
生产 HTTPS 下 cookie 名自动启用 `__Host-` 前缀。
若本机曾跑过早期构建,浏览器里可能还留着 `Path=/api/auth` 的 `j13_refresh`(HttpOnly,页面脚本删不掉)。打开开发者工具 → Application → Cookies,删掉该条,或对该源执行「清除站点数据」。之后只应存在 `Path=/` 的三枚 cookie。
---
## 部署
**官方运行时是 `deploy/docker-compose.yml`(Next SSR + Go + Postgres,不含反代)。** 说明与升级命令见 [deploy/README.md](./deploy/README.md),Nginx 示例见 [deploy/nginx.example.conf](./deploy/nginx.example.conf),契约见 [deploy/CONTRACT.md](./deploy/CONTRACT.md)。
本地开发仍用仓库根 `start.bat` / `start.sh` 与根目录 `docker-compose.yml`(只起 Postgres)。不要把开发流程改成必须打应用镜像。
更新应用(镜像已发布时):
```bash
cd deploy
docker compose pull
docker compose up -d
```
后台一键更新、预编译包不在本期。Cloudflare Workers 不是官方运行时。
### 生产环境要点
1. **官方 Compose:** `DEV_MODE=false`,强随机 `JWT_SECRET` 与 `POSTGRES_PASSWORD`;由你现有的 Nginx/Caddy 提供 HTTPS,并把页面与 `/api`、`/api/ws`、`/uploads` 挂到同一 Host。
2. **CORS:** 设 `SITE_URL` 为对外 origin(不要改 `router.go` 写死域名)。
3. **自管部署:** 仍须同源反代。Workers 前端须保持 `middleware.ts` 文件名(OpenNext 不识别 `proxy.ts`),实时通道由反代承接 `/api/ws`。
### WebSocket(开发 vs 生产)
浏览器发起 WebSocket 握手**一定会带 `Origin`**。后端拒绝空 Origin,这只挡住 curl / 脚本探测,**不影响 `npm run dev`,也不影响生产浏览器**。
**开发环境(`DEV_MODE=true`)**
- 页面在 `http://localhost:3000`,WS 直连 `ws://localhost:3001/api/ws`(Next.js rewrite 不透传 `Upgrade`)。
- 二者端口不同,不是浏览器意义上的同源。后端因此放行 Origin 主机为 `localhost` / `127.0.0.1` 的**任意端口**。
- Cookie 不按端口隔离,`:3000` 上的 `j13_token` 会随握手带到 `:3001`。不要把 `DEV_MODE` 改成 `false` 再跑这对端口,否则 Origin `localhost:3000` 对不上 Host `localhost:3001`,握手会被拒。
**生产环境(`DEV_MODE=false`)**
- Origin 的 host(含端口)必须与请求 `Host` **完全一致**。请用 Nginx/Caddy 把前端与 `/api/`、`/api/ws` 挂到同一域名,例如 `https://bbs.example.com`。
- 反向代理需透传 `Upgrade`、`Connection`,并保留原始 `Host` / `Origin`。
- 生产 cookie 使用 `__Host-` 前缀,要求 `Secure`、`Path=/`、**不能设 Domain**。因此 API **不能**放到 `api.example.com` 这类另一子域,否则登录 cookie 与 WS 都会失效。
- Cloudflare Workers / OpenNext **不能**透传任意 WebSocket。若前端部署在 Workers,实时通道必须由同源反代(Nginx 等)承接 `/api/ws`,而不是指望 Workers 升级连接。
- 可用 `NEXT_PUBLIC_WS_URL` 覆盖前端连线地址;一旦覆盖,Origin 仍须与后端看到的 Host 一致。
---
## 开发约定
- **Next.js 16:** `params` / `searchParams` / `cookies()` / `headers()` 必须 `await`。
- **SEO:** 所有公开页面 SSR/ISR,SEO 关键内容(title、description、OG、JSON-LD)必须在初始 HTML 中。
- **权限:** 权限校验只在 Go 后端执行,前端不做判定。
- **配色:** 仅修改 `frontend/app/globals.css` 中的 CSS 令牌,禁止在组件内硬编码颜色。
- **无障碍:** 文字对比度 ≥4.5:1,交互控件边界 ≥3:1(WCAG 2.1 AA)。
---
## 许可证
**专有软件(Proprietary)** — Copyright © 2026 姜十三,保留所有权利。
源码仅可见,未经书面许可不得使用、部署、修改或分发。详见 [LICENSE](./LICENSE)。
联系方式:aarbbs@88.com