From 711cd8b7597d79c089f355ab961c514cb5c0d3f6 Mon Sep 17 00:00:00 2001 From: freefire Date: Tue, 8 Sep 2026 23:54:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B8=96=E5=AD=90=E5=85=A8=E5=B1=80/?= =?UTF-8?q?=E6=9D=BF=E5=9D=97=E7=BD=AE=E9=A1=B6=E6=94=AF=E6=8C=81=E6=9D=83?= =?UTF-8?q?=E9=87=8D=E6=8E=92=E5=BA=8F=EF=BC=8C=E4=BC=98=E5=8C=96=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E4=B8=8E=E8=AF=A6=E6=83=85=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/site-posts/README.md | 8 + docs/site-posts/deployment-guide.md | 244 ++++++++++++++++++ frontend/src/api/client.ts | 8 +- frontend/src/api/types.ts | 4 +- frontend/src/components/PostListItem.tsx | 4 +- frontend/src/components/PostManageMenu.tsx | 4 +- frontend/src/components/StaticFeedList.tsx | 4 +- frontend/src/pages/PostDetailPage.tsx | 10 +- .../src/pages/admin/AdminDashboardPage.tsx | 6 +- frontend/src/pages/admin/AdminPostsPage.tsx | 72 +++++- frontend/src/styles/global.css | 11 +- frontend/src/utils/enhanceCodeBlocks.ts | 21 +- handler/api.go | 12 +- handler/ssr_home.go | 4 +- model/models.go | 4 +- service/post.go | 10 +- 16 files changed, 362 insertions(+), 64 deletions(-) create mode 100644 docs/site-posts/deployment-guide.md diff --git a/docs/site-posts/README.md b/docs/site-posts/README.md index 75d3b72..8c19a20 100644 --- a/docs/site-posts/README.md +++ b/docs/site-posts/README.md @@ -28,6 +28,14 @@ 访问路径:`/page/changelog`。 +## 部署指南(置顶帖) + +| 文件 | 标题 | 标签 | 操作 | +| --- | --- | --- | --- | +| [deployment-guide.md](./deployment-guide.md) | 从零开始部署姜十三论坛 | `部署` `教程` | **置顶**(可选精华) | + +覆盖:服务器选型、宝塔 / 1Panel、Docker / 二进制、域名 HTTPS、备份升级、常见问题。 + ## 可选短文 若站务区希望不止一篇,见 [optional.md](./optional.md): diff --git a/docs/site-posts/deployment-guide.md b/docs/site-posts/deployment-guide.md new file mode 100644 index 0000000..ebf1dde --- /dev/null +++ b/docs/site-posts/deployment-guide.md @@ -0,0 +1,244 @@ +# 从零开始部署姜十三论坛 + +> 建议标题:从零开始部署姜十三论坛 +> 建议 slug:`deploy` +> 建议操作:发布后设为「置顶」 +> 复制正文时:从下方第一个 `---` **之后**开始粘贴 + +--- + +## 为什么选姜十三 + +单二进制 + 内置 SQLite,**不需要 MySQL、PHP、Redis**。面板(宝塔 / 1Panel)只是帮你更方便地管理,不是必须的。 + +## 服务器选型 + +| 部署方式 | CPU | 内存 | 磁盘 | +| --- | --- | --- | --- | +| 纯二进制(无面板) | 1 核 | 512MB | 10GB SSD | +| 二进制 + 宝塔 | 1 核 | 1GB | 20GB SSD | +| Docker + 1Panel | 2 核 | 2GB | 40GB SSD | + +> 推荐 Debian 12 / Ubuntu 22.04。建议直接上 2 核 2G。 + +## 部署方式对比 + +| | 1Panel + Docker | 宝塔 + 二进制 | 纯命令行 Docker | +| --- | --- | --- | --- | +| 适合 | 新手 / 还要跑其他容器 | 习惯宝塔 / 只跑这一个 | 熟悉 Linux / 资源紧张 | +| 资源占用 | 较高(自带 Docker) | 低 | 最低 | +| HTTPS | 面板一键 | 面板一键 | Caddy / certbot | +| 备份 | 面板计划任务 | 面板计划任务 | `tar` 打包 | +| 升级 | 面板点一下 | 替换二进制 | `docker pull` + 重启 | + +## 前置准备 + +```bash +apt update && apt upgrade -y +apt install -y curl wget ca-certificates +timedatectl set-timezone Asia/Shanghai +``` + +--- + +## 方式一:1Panel + Docker + +### 1. 安装 1Panel + +```bash +curl -sSL https://resource.fit2cloud.com/1panel/package/v2/quick_start.sh \ + -o quick_start.sh && bash quick_start.sh +``` + +- 安装目录:默认 `/opt` +- 是否装 Docker:**y**(必须) +- 面板端口 / 安全入口 / 账号密码自定义,记好 + +安全组放行设置的端口,然后浏览器打开面板。 + +### 2. 启动姜十三 + +1Panel 面板 → 容器 → 创建: + +| 字段 | 值 | +| --- | --- | +| 镜像 | `hangzhang714128/jiang13-forum:latest` | +| 端口 | `3000:3000` | +| 数据卷 | 主机目录 → 容器内 **`/data`** | +| 重启策略 | `unless-stopped` | + +浏览器访问 `http://IP:3000` → **首个注册用户自动成为管理员**。 + +> 若容器报 `permission denied`:`chown -R 1000:1000 /数据目录` + +--- + +## 方式二:宝塔 + 二进制 + +### 1. 安装宝塔 + +```bash +# 正式版 13.0.0(含 AI,需绑定账号) +if [ -f /usr/bin/curl ];then curl -sSO https://download.bt.cn/install/install_panel.sh;else wget -O install_panel.sh https://download.bt.cn/install/install_panel.sh;fi;bash install_panel.sh KZOqff1mn + +# 稳定版 12.0.0(纯净、免绑定) +url=https://download.bt.cn/install/installStable_12.sh;if [ -f /usr/bin/curl ];then curl -sSO $url;else wget -O installStable_12.sh $url;fi;bash installStable_12.sh 5x59vxy56 +``` + +安全组放行随机端口,浏览器打开(自签名证书提示 → 高级 → 继续前往)。 + +### 2. 下载并注册服务 + +```bash +mkdir -p /opt/jiang13 && cd /opt/jiang13 +# 版本号以更新日志为准:https://bbs.iioio.com/page/update-log.html +wget -O jiang13 https://git.iioio.com/freefire/jiang13-forum/releases/download/v1.1.8/jiang13_v1.1.8_linux-amd64 +chmod +x jiang13 + +# 注册为 systemd 服务(install 创建 unit 文件,start 启动,enable 开机自启) +./jiang13 -service install +./jiang13 -service start +systemctl enable jiang13 + +# 日常管理 +./jiang13 -service status # 查看状态 +./jiang13 -service restart # 重启 + +# 验证 +curl http://127.0.0.1:3000/health +``` + +> 只是临时跑一下?直接 `./jiang13` 前台启动即可,Ctrl+C 停止,不用 `install`。 + +### 2.1 升级二进制 + +```bash +cd /opt/jiang13 +./jiang13 -service stop +wget -O jiang13.new https://git.iioio.com/freefire/jiang13-forum/releases/download/v新版本/jiang13_v新版本_linux-amd64 +mv jiang13.new jiang13 && chmod +x jiang13 +./jiang13 -service start +``` + +### 3. 宝塔反代 + SSL + +网站 → 添加站点(域名,PHP 选**纯静态**)→ 反向代理(目标 `http://127.0.0.1:3000`)→ SSL → Let's Encrypt 一键签发 → 强制 HTTPS。 + +--- + +## 方式三:纯 Docker + +```bash +# 安装 Docker +bash <(curl -sSL https://linuxmirrors.cn/docker.sh) + +# 启动姜十三 +docker run -d \ + --name jiang13 \ + -p 3000:3000 \ + -v /opt/jiang13-data:/data \ + --restart unless-stopped \ + hangzhang714128/jiang13-forum:latest +``` + +或用 docker compose: + +```yaml +services: + jiang13: + image: hangzhang714128/jiang13-forum:latest + ports: ["3000:3000"] + volumes: ["./data:/data"] + environment: {TZ: Asia/Shanghai} + restart: unless-stopped +``` + +```bash +docker compose up -d +``` + +HTTPS 用 Caddy 最省心: + +``` +bbs.yourdomain.com { + reverse_proxy 127.0.0.1:3000 +} +``` + +Caddy 自动申请 Let's Encrypt。 + +--- + +## 域名 + HTTPS + +域名服务商添加 **A 记录**:`bbs` → 服务器公网 IP。 + +| 方式 | HTTPS 操作 | +| --- | --- | +| 1Panel | 网站 → 创建 → 反向代理 → SSL → Let's Encrypt | +| 宝塔 | 同上 | +| 纯 Docker | Caddy 自动 / Nginx + certbot | + +--- + +## 数据备份 + +所有数据(SQLite + 上传 + JWT 密钥)都在**一个目录**里,备份 = 复制目录。 + +| 方式 | 数据目录 | +| --- | --- | +| 1Panel Docker | 你映射的宿主机目录 | +| 宝塔二进制 | 二进制同目录下 `data/` | +| 纯 Docker | `-v` 映射的目录 | + +```bash +tar -czf jiang13-backup-$(date +%Y%m%d).tar.gz /数据目录 +``` + +> ⚠️ `data/.jwt_secret` 务必一起备份,丢了所有用户已登录的 cookie 会失效,需要重新输入密码登录(密码本身存 SQLite 里不受影响)。 + +--- + +## 版本升级 + +版本号以 [更新日志](https://bbs.iioio.com/page/update-log.html) 为准。 + +| 方式 | 升级 | +| --- | --- | +| 1Panel | 容器列表 → 选中 → 镜像更新 | +| docker compose | `docker compose pull && docker compose up -d` | +| 二进制 | 见上方「2.1 升级二进制」 | + +升级前先备份:`tar -czf backup.tar.gz /数据目录` + +--- + +## 面板常用命令 + +| 操作 | 宝塔 | 1Panel | +| --- | --- | --- | +| 菜单 | `bt` | `1pctl` | +| 查入口 / 密码 | `bt default` | `1pctl user-info` | +| 改密码 | `bt 14` | `1pctl update password` | +| 重启面板 | `bt restart` | — | + +## 常见问题 + +**Q:面板 / 网站打不开?** 安全组有没有放行对应端口?`netstat -tlnp | grep 端口` 看服务在不在监听。 + +**Q:宝塔 vs 1Panel 怎么选?** 只跑姜十三 → 宝塔 + 二进制(省资源);还要跑别的 → 1Panel。 + +**Q:支持邮件 / SSO 吗?** 邮件 SMTP 和 OIDC SSO 都在**管理后台 → 系统设置**里配置。 + +**Q:源码编译?** 见项目 README。推荐用预编译包。 + +## 链接 + +| 项目 | 地址 | +| --- | --- | +| 在线演示 | https://bbs.iioio.com/ | +| 更新日志 | https://bbs.iioio.com/page/update-log.html | +| Docker Hub | hangzhang714128/jiang13-forum | +| 预编译下载 | https://git.iioio.com/freefire/jiang13-forum/releases | + +有问题欢迎本帖留言。 diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index b3fddd3..d39cfbb 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -154,12 +154,12 @@ export const api = { }, adminApprovePost: (id: number) => request<{ message: string; status: string }>(`/api/admin/posts/${id}/approve`, { method: 'POST' }), - adminPinPost: (id: number, pinned: boolean) => - request<{ message: string; pinned: boolean }>(`/api/admin/posts/${id}/pin`, { + adminPinPost: (id: number, pinned: number) => + request<{ message: string; pinned: number }>(`/api/admin/posts/${id}/pin`, { method: 'POST', body: JSON.stringify({ pinned }), }), - adminBoardPinPost: (id: number, boardPinned: boolean) => - request<{ message: string; board_pinned: boolean }>(`/api/admin/posts/${id}/board-pin`, { + adminBoardPinPost: (id: number, boardPinned: number) => + request<{ message: string; board_pinned: number }>(`/api/admin/posts/${id}/board-pin`, { method: 'POST', body: JSON.stringify({ board_pinned: boardPinned }), }), adminFeaturePost: (id: number, featured: boolean) => diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index d99e195..eef91fb 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -94,9 +94,9 @@ export interface PostItem { bounty_comment_id?: number; lottery_winner_count?: number; lottery_status?: 'open' | 'drawn' | string; - pinned: boolean; + pinned: number; /** 板块内置顶(仅板块列表抬升,首页不抬升) */ - board_pinned?: boolean; + board_pinned?: number; featured?: boolean; edit_locked?: boolean; /** 禁止新评论(结贴) */ diff --git a/frontend/src/components/PostListItem.tsx b/frontend/src/components/PostListItem.tsx index d53af3a..eb4e7a6 100644 --- a/frontend/src/components/PostListItem.tsx +++ b/frontend/src/components/PostListItem.tsx @@ -71,10 +71,10 @@ function PostListItem({ post, sort = 'latest', boardId = 0, onSelect }: Props) { const titleRow = (
- {post.pinned && ( + {post.pinned > 0 && ( 全局置顶 )} - {post.board_pinned && ( + {(post.board_pinned ?? 0) > 0 && ( 板块置顶 )} {post.featured && ( diff --git a/frontend/src/components/PostManageMenu.tsx b/frontend/src/components/PostManageMenu.tsx index f058729..5619e74 100644 --- a/frontend/src/components/PostManageMenu.tsx +++ b/frontend/src/components/PostManageMenu.tsx @@ -151,11 +151,11 @@ export default function PostManageMenu({ - {post.pinned ? '取消全局置顶' : '全局置顶'} + {post.pinned > 0 ? '取消全局置顶' : '全局置顶'} - {post.board_pinned ? '取消板块置顶' : '板块置顶'} + {(post.board_pinned ?? 0) > 0 ? '取消板块置顶' : '板块置顶'} diff --git a/frontend/src/components/StaticFeedList.tsx b/frontend/src/components/StaticFeedList.tsx index 74d6721..61487d4 100644 --- a/frontend/src/components/StaticFeedList.tsx +++ b/frontend/src/components/StaticFeedList.tsx @@ -98,10 +98,10 @@ export default function StaticFeedList({ posts, sort, boardId }: Props) { const titleRow = (
- {post.pinned ? ( + {post.pinned > 0 ? ( 全局置顶 ) : null} - {post.board_pinned ? ( + {(post.board_pinned ?? 0) > 0 ? ( 板块置顶 ) : null} {post.featured ? 推荐 : null} diff --git a/frontend/src/pages/PostDetailPage.tsx b/frontend/src/pages/PostDetailPage.tsx index 4fbb015..ba0ba85 100644 --- a/frontend/src/pages/PostDetailPage.tsx +++ b/frontend/src/pages/PostDetailPage.tsx @@ -806,7 +806,8 @@ export default function PostDetailPage() { const handlePin = async () => { if (!post) return; try { - const r = await api.adminPinPost(postId, !post.pinned); + const next = post.pinned > 0 ? 0 : 1; + const r = await api.adminPinPost(postId, next); setPost(p => p ? { ...p, pinned: r.pinned } : p); clearAllFeedCache(); window.dispatchEvent(new Event('posts-refresh')); @@ -819,7 +820,8 @@ export default function PostDetailPage() { const handleBoardPin = async () => { if (!post) return; try { - const r = await api.adminBoardPinPost(postId, !post.board_pinned); + const next = (post.board_pinned ?? 0) > 0 ? 0 : 1; + const r = await api.adminBoardPinPost(postId, next); setPost(p => p ? { ...p, board_pinned: r.board_pinned } : p); clearAllFeedCache(); window.dispatchEvent(new Event('posts-refresh')); @@ -995,10 +997,10 @@ export default function PostDetailPage() {

- {post.pinned && ( + {post.pinned > 0 && ( 全局置顶 )} - {post.board_pinned && ( + {(post.board_pinned ?? 0) > 0 && ( 板块置顶 )} {post.featured && } diff --git a/frontend/src/pages/admin/AdminDashboardPage.tsx b/frontend/src/pages/admin/AdminDashboardPage.tsx index 10f929f..d324b96 100644 --- a/frontend/src/pages/admin/AdminDashboardPage.tsx +++ b/frontend/src/pages/admin/AdminDashboardPage.tsx @@ -246,9 +246,9 @@ export default function AdminDashboardPage() { {p.featured ? 推荐 : null} - {p.pinned ? 全局置顶 : null} - {p.board_pinned ? 板块置顶 : null} - {!p.featured && !p.pinned && !p.board_pinned ? '—' : null} + {p.pinned > 0 ? 全局置顶 : null} + {(p.board_pinned ?? 0) > 0 ? 板块置顶 : null} + {!p.featured && p.pinned <= 0 && (p.board_pinned ?? 0) <= 0 ? '—' : null} {new Date(p.created_at).toLocaleString('zh-CN')} diff --git a/frontend/src/pages/admin/AdminPostsPage.tsx b/frontend/src/pages/admin/AdminPostsPage.tsx index 8d74770..25b353b 100644 --- a/frontend/src/pages/admin/AdminPostsPage.tsx +++ b/frontend/src/pages/admin/AdminPostsPage.tsx @@ -141,9 +141,13 @@ export default function AdminPostsPage() { setSearch(''); }; - const togglePin = async (post: PostItem) => { + const togglePin = async (post: PostItem, nextValue?: number) => { try { - const r = await api.adminPinPost(post.id, !post.pinned); + let value = nextValue; + if (value === undefined) { + value = post.pinned > 0 ? 0 : 1; + } + const r = await api.adminPinPost(post.id, value); clearAllFeedCache(); window.dispatchEvent(new Event('posts-refresh')); notify.success(r.message); @@ -153,9 +157,13 @@ export default function AdminPostsPage() { } }; - const toggleBoardPin = async (post: PostItem) => { + const toggleBoardPin = async (post: PostItem, nextValue?: number) => { try { - const r = await api.adminBoardPinPost(post.id, !post.board_pinned); + let value = nextValue; + if (value === undefined) { + value = (post.board_pinned ?? 0) > 0 ? 0 : 1; + } + const r = await api.adminBoardPinPost(post.id, value); clearAllFeedCache(); window.dispatchEvent(new Event('posts-refresh')); notify.success(r.message); @@ -417,8 +425,8 @@ export default function AdminPostsPage() { {p.tags || '—'} {p.comment_count ?? 0} {p.featured ? 是 : '—'} - {p.pinned ? 是 : '—'} - {p.board_pinned ? 是 : '—'} + {p.pinned > 0 ? 置顶 {p.pinned} : '—'} + {(p.board_pinned ?? 0) > 0 ? 置顶 {p.board_pinned} : '—'} {p.edit_locked ? 是 : '—'} {p.comments_locked ? 是 : '—'} {p.like_count} @@ -444,11 +452,55 @@ export default function AdminPostsPage() { - - '); + const langHtml = label ? `${escapeHtml(label)}` : ''; head.innerHTML = ` - ${escapeHtml(label)} + ${langHtml} ${actions.join('')} `; diff --git a/handler/api.go b/handler/api.go index 38b0e66..10a8d18 100644 --- a/handler/api.go +++ b/handler/api.go @@ -244,11 +244,11 @@ func (h *Handlers) APIAdminCommentsLockPost(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": msg, "comments_locked": req.Locked}) } -// APIAdminPinPost 全局置顶/取消全局置顶(JSON) +// APIAdminPinPost 全局置顶/取消全局置顶(JSON),支持设置置顶顺序数值 func (h *Handlers) APIAdminPinPost(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) var req struct { - Pinned bool `json:"pinned"` + Pinned int `json:"pinned"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) @@ -259,17 +259,17 @@ func (h *Handlers) APIAdminPinPost(c *gin.Context) { return } msg := "已取消全局置顶" - if req.Pinned { + if req.Pinned > 0 { msg = "已全局置顶" } c.JSON(http.StatusOK, gin.H{"message": msg, "pinned": req.Pinned}) } -// APIAdminBoardPinPost 板块内置顶/取消板块内置顶(JSON) +// APIAdminBoardPinPost 板块内置顶/取消板块内置顶(JSON),支持设置置顶顺序数值 func (h *Handlers) APIAdminBoardPinPost(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 64) var req struct { - BoardPinned bool `json:"board_pinned"` + BoardPinned int `json:"board_pinned"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"}) @@ -280,7 +280,7 @@ func (h *Handlers) APIAdminBoardPinPost(c *gin.Context) { return } msg := "已取消板块置顶" - if req.BoardPinned { + if req.BoardPinned > 0 { msg = "已板块置顶" } c.JSON(http.StatusOK, gin.H{"message": msg, "board_pinned": req.BoardPinned}) diff --git a/handler/ssr_home.go b/handler/ssr_home.go index ab478cc..db1c157 100644 --- a/handler/ssr_home.go +++ b/handler/ssr_home.go @@ -673,10 +673,10 @@ func writeSSRPostRow( } b.WriteString(`
`) - if post.Pinned { + if post.Pinned > 0 { b.WriteString(`全局置顶`) } - if post.BoardPinned { + if post.BoardPinned > 0 { b.WriteString(`板块置顶`) } if post.Featured { diff --git a/model/models.go b/model/models.go index 91f2ca2..e6a324a 100644 --- a/model/models.go +++ b/model/models.go @@ -113,8 +113,8 @@ type Post struct { BountyCommentID uint `gorm:"default:0" json:"bounty_comment_id"` // 采纳的评论 LotteryWinnerCount int `gorm:"default:1" json:"lottery_winner_count"` // 抽奖人数(仅 lottery) LotteryStatus string `gorm:"size:16;default:'';index" json:"lottery_status"` // open|drawn - Pinned bool `gorm:"default:false" json:"pinned"` // 全局置顶 - BoardPinned bool `gorm:"default:false" json:"board_pinned"` // 板块内置顶 + Pinned int `gorm:"default:0" json:"pinned"` // 全局置顶(0=未置顶,>0=置顶,数值越大越靠前) + BoardPinned int `gorm:"default:0" json:"board_pinned"` // 板块内置顶(0=未置顶,>0=置顶,数值越大越靠前) Featured bool `gorm:"default:false;index" json:"featured"` // 推荐帖(原精华) EditLocked bool `gorm:"default:false" json:"edit_locked"` CommentsLocked bool `gorm:"default:false" json:"comments_locked"` // 禁止评论(结贴) diff --git a/service/post.go b/service/post.go index 52eeb38..3e2d2f4 100644 --- a/service/post.go +++ b/service/post.go @@ -813,11 +813,17 @@ func (s *PostService) Purge(postID uint) error { }) } -func (s *PostService) SetPinned(postID uint, pinned bool) error { +func (s *PostService) SetPinned(postID uint, pinned int) error { + if pinned < 0 { + pinned = 0 + } return model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("pinned", pinned).Error } -func (s *PostService) SetBoardPinned(postID uint, boardPinned bool) error { +func (s *PostService) SetBoardPinned(postID uint, boardPinned int) error { + if boardPinned < 0 { + boardPinned = 0 + } return model.DB.Model(&model.Post{}).Where("id = ?", postID).Update("board_pinned", boardPinned).Error }