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() {
-