From bdc8e6d82aacf5c691f6a659acf2c4d14e01cc55 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 17 Jul 2026 14:31:24 +0800 Subject: [PATCH 01/14] =?UTF-8?q?ci:=20=E6=94=B9=E4=B8=BA=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=9C=BA=E6=9E=84=E5=BB=BA=E6=96=B9=E6=A1=88=EF=BC=8C?= =?UTF-8?q?=E9=80=82=E9=85=8D=20docker=20=E6=A8=A1=E5=BC=8F=20runner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runner 不再 host 模式跑 docker build(原 host runner 缺 node/docker 无法执行) - 新流程:runner 打包源码 → scp 到 132 → 132 上 docker build + compose up - runner 只需 catthehacker 镜像(node/git/sshpass),docker 能力下沉到 132 - deploy/README 补 runner docker 模式部署命令(挂 sock + 持久化 /data) Co-Authored-By: Claude Opus 4.8 --- .gitea/workflows/deploy.yml | 47 ++++++++++++++++++------------------- deploy/README.md | 26 +++++++++++++++----- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index cb76319..bea7efc 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -7,7 +7,6 @@ on: workflow_dispatch: env: - IMAGE: sundynix-site:latest REMOTE_DIR: /home/workspace/sundynix-site jobs: @@ -17,21 +16,18 @@ jobs: - name: 检出代码 uses: actions/checkout@v4 - - name: 构建镜像 - run: docker build -t "$IMAGE" . - - - name: 导出镜像为压缩包 - run: docker save "$IMAGE" | gzip > image.tar.gz - - name: 安装 ssh 工具 run: | if command -v apt-get >/dev/null; then apt-get update && apt-get install -y sshpass openssh-client - elif command -v apk >/dev/null; then - apk add --no-cache sshpass openssh-client + else + apk add --no-cache sshpass openssh-client tar fi - - name: 传输镜像与 compose 到部署机 + - name: 打包源码 + run: tar czf /tmp/src.tar.gz --exclude=./.git --exclude=node_modules . + + - name: 传输源码到部署机 env: SSHPASS: ${{ secrets.DEPLOY_PASSWORD }} run: | @@ -39,20 +35,23 @@ jobs: U="${{ secrets.DEPLOY_USER }}" sshpass -e ssh -o StrictHostKeyChecking=no "$U@$H" "mkdir -p $REMOTE_DIR" sshpass -e scp -o StrictHostKeyChecking=no \ - image.tar.gz docker-compose.yml "$U@$H:$REMOTE_DIR/" + /tmp/src.tar.gz docker-compose.yml "$U@$H:$REMOTE_DIR/" - - name: 部署机加载镜像并重启 + - name: 部署机构建并重启 env: SSHPASS: ${{ secrets.DEPLOY_PASSWORD }} run: | H="${{ secrets.DEPLOY_HOST }}" U="${{ secrets.DEPLOY_USER }}" - sshpass -e ssh -o StrictHostKeyChecking=no "$U@$H" "\ - cd $REMOTE_DIR && \ - gunzip -c image.tar.gz | docker load && \ - rm -f image.tar.gz && \ - docker compose up -d && \ - docker image prune -f" + sshpass -e ssh -o StrictHostKeyChecking=no "$U@$H" ' + set -e + cd /home/workspace/sundynix-site + rm -rf build && mkdir build + tar xzf src.tar.gz -C build + docker build -t sundynix-site:latest build + docker compose up -d + rm -rf build src.tar.gz + docker image prune -f' - name: 健康检查 env: @@ -60,9 +59,9 @@ jobs: run: | H="${{ secrets.DEPLOY_HOST }}" U="${{ secrets.DEPLOY_USER }}" - sshpass -e ssh -o StrictHostKeyChecking=no "$U@$H" "\ - for i in \$(seq 1 15); do \ - wget -qO- http://127.0.0.1:8090/api/healthz && exit 0; \ - sleep 2; \ - done; \ - echo '健康检查失败'; docker logs --tail 50 sundynix-site; exit 1" + sshpass -e ssh -o StrictHostKeyChecking=no "$U@$H" ' + for i in $(seq 1 20); do + curl -sf http://127.0.0.1:8090/api/healthz && exit 0 + sleep 3 + done + echo "健康检查失败"; docker logs --tail 50 sundynix-site; exit 1' diff --git a/deploy/README.md b/deploy/README.md index 1fc4ec1..ea88882 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -6,11 +6,11 @@ push/merge main ─▶ Gitea(192.168.100.125:3000) │ 触发 .gitea/workflows/deploy.yml ▼ - act_runner(192.168.100.128) - │ docker build → save → scp + act_runner(192.168.100.128, docker 模式) + │ 打包源码 → scp ▼ 部署机(192.168.100.132) /home/workspace/sundynix-site - │ docker load → compose up → 容器 :8090 + │ docker build → compose up → 容器 :8090 │ │ 连 │ MySQL(192.168.100.127:3307) ▼ @@ -39,10 +39,24 @@ vi .env # 改 admin 密码、JWT 密钥 `.env` 关键项见 [.env.production.example](../.env.production.example)。数据库指向内网 `192.168.100.127:3307/sundynix_site`(库不存在会自动建表 + 种子)。 -### 3. act_runner 128 要求 +### 3. act_runner 128 要求(docker 模式,非 host) -- 能访问 docker daemon(`docker build/save` 可用) -- 能 ssh 到 132(workflow 用 sshpass 走密码) +runner 必须用 **docker 模式** + 挂 docker.sock + 持久化 /data,标签用带 node 的镜像: + +```bash +docker run -d --name act_runner --restart unless-stopped \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /root/act_runner_data:/data \ + -e GITEA_INSTANCE_URL=http://192.168.100.125:3000 \ + -e GITEA_RUNNER_REGISTRATION_TOKEN=<在 gitea 运行器页创建新运行器获取> \ + -e GITEA_RUNNER_NAME=Auto-CICD \ + -e "GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:act-22.04" \ + gitea/act_runner:0.6.1 +``` + +- job 在 catthehacker 镜像里跑(含 node/git/apt,checkout 与 sshpass 可用) +- 真正的 `docker build` 在 132 上执行,128 只需能 ssh 到 132 +- 部署机 132 需装 docker + compose 插件 ### 4. 公网服务器 nginx -- 2.52.0 From 86df16804ad2b892f51ae154291a346d49c03f84 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 17 Jul 2026 14:46:54 +0800 Subject: [PATCH 02/14] =?UTF-8?q?ci:=20=E8=A7=A6=E5=8F=91=E9=83=A8?= =?UTF-8?q?=E7=BD=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -- 2.52.0 From 644f9e6957a76138c8b42df1ae008e6c374f7fe2 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 17 Jul 2026 15:25:56 +0800 Subject: [PATCH 03/14] =?UTF-8?q?ci:=20Dockerfile=20=E6=8D=A2=E5=9B=BD?= =?UTF-8?q?=E5=86=85=E6=BA=90=E5=8A=A0=E9=80=9F=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - npm 用 npmmirror(淘宝源),go 用 goproxy.cn - 大幅缩短 132 上 npm ci / go mod download 耗时 Co-Authored-By: Claude Opus 4.8 --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 019541b..e2b6b51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,20 +2,21 @@ FROM node:20-alpine AS web WORKDIR /app/web COPY web/package*.json ./ -RUN npm ci +RUN npm config set registry https://registry.npmmirror.com && npm ci COPY web/ ./ RUN npm run build FROM node:20-alpine AS admin WORKDIR /app/admin COPY admin/package*.json ./ -RUN npm ci +RUN npm config set registry https://registry.npmmirror.com && npm ci COPY admin/ ./ RUN npm run build # ── 2. 编译 Go(embed 两份前端产物)── FROM golang:1.26-alpine AS server WORKDIR /app/server +ENV GOPROXY=https://goproxy.cn,direct COPY server/go.mod server/go.sum ./ RUN go mod download COPY server/ ./ -- 2.52.0 From 2dfe124b863c127cf438e1938ca8e75dd1a5cfb0 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 17 Jul 2026 15:35:48 +0800 Subject: [PATCH 04/14] =?UTF-8?q?ci:=20checkout=20=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E5=86=85=E7=BD=91=20gitea=20git=20clone=EF=BC=8C=E5=8E=BB?= =?UTF-8?q?=E9=99=A4=20github=20=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - actions/checkout@v4 从 github.com 拉取,内网 TLS 握手超时 - 改为直接 git clone 内网 gitea(192.168.100.125:3000),用 github.token 认证 - 至此 workflow 无任何 github action 依赖,全程走内网 Co-Authored-By: Claude Opus 4.8 --- .gitea/workflows/deploy.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index bea7efc..6536e8a 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -14,7 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - name: 检出代码 - uses: actions/checkout@v4 + run: | + git config --global --add safe.directory '*' + git clone "http://token:${{ github.token }}@192.168.100.125:3000/${{ github.repository }}.git" . + git checkout "${{ github.sha }}" - name: 安装 ssh 工具 run: | -- 2.52.0 From 7f050843f9f7249c15fddf90dd8f0cee68defc0f Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 17 Jul 2026 15:48:07 +0800 Subject: [PATCH 05/14] =?UTF-8?q?docs:=20=E4=BF=AE=E6=AD=A3=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E5=BA=93=E5=AF=86=E7=A0=81=E4=B8=BA=20root=EF=BC=88My?= =?UTF-8?q?SQL=20root=20=E5=AF=86=E7=A0=81=20!=3D=20=E6=9C=BA=E5=99=A8=20S?= =?UTF-8?q?SH=20=E5=AF=86=E7=A0=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .env.production.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.production.example b/.env.production.example index b9cbdd5..dbde03d 100644 --- a/.env.production.example +++ b/.env.production.example @@ -2,8 +2,8 @@ # 一次性手动放置,CI 不覆盖它(敏感信息不进流水线) # compose 的 env_file 读取本文件 -# 内网 MySQL(127 那台),库名 sundynix_site -SUNDYNIX_DB=root:sundynix@tcp(192.168.100.127:3307)/sundynix_site?charset=utf8mb4&parseTime=True&loc=Local +# 内网 MySQL(127 那台,MySQL root 密码是 root,注意不是机器 SSH 密码),库名 sundynix_site +SUNDYNIX_DB=root:root@tcp(192.168.100.127:3307)/sundynix_site?charset=utf8mb4&parseTime=True&loc=Local # 监听地址(容器内固定 8090,勿改,与 compose ports 对应) SUNDYNIX_ADDR=:8090 -- 2.52.0 From 6334dccf4a0c5084c8cd7a7cb4545b5de5e1906b Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 17 Jul 2026 16:31:37 +0800 Subject: [PATCH 06/14] =?UTF-8?q?feat(admin):=20=E4=BE=A7=E8=BE=B9?= =?UTF-8?q?=E6=A0=8F=E5=B8=83=E5=B1=80=20+=20=E4=BB=AA=E8=A1=A8=E7=9B=98?= =?UTF-8?q?=20+=20=E5=85=A8=E5=B1=80=E9=9A=90=E8=97=8F=E6=BB=9A=E5=8A=A8?= =?UTF-8?q?=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 全局隐藏滚动条(web + admin),保留正常滚动 - admin 顶栏改侧边栏导航(仪表盘/文章),响应式移动端抽屉 - 路由重构:首页→仪表盘,文章列表移到 /posts - 仪表盘页:文章总数/已发布/草稿统计卡片 + 最近更新列表 - 后端 GET /api/admin/stats Co-Authored-By: Claude Opus 4.8 --- .claude/launch.json | 7 ++ admin/src/App.tsx | 4 +- admin/src/api/posts.ts | 11 +++ admin/src/components/admin-layout.tsx | 73 +++++++--------- admin/src/components/admin-sidebar.tsx | 109 +++++++++++++++++++++++ admin/src/index.css | 11 +++ admin/src/pages/dashboard.tsx | 114 +++++++++++++++++++++++++ server/internal/handler/admin_post.go | 20 +++++ server/internal/router/router.go | 1 + web/src/index.css | 11 +++ 10 files changed, 316 insertions(+), 45 deletions(-) create mode 100644 admin/src/components/admin-sidebar.tsx create mode 100644 admin/src/pages/dashboard.tsx diff --git a/.claude/launch.json b/.claude/launch.json index 3263e92..b62ef28 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -7,6 +7,13 @@ "runtimeArgs": ["run", "dev"], "cwd": "web", "port": 5173 + }, + { + "name": "admin", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "cwd": "admin", + "port": 5174 } ] } diff --git a/admin/src/App.tsx b/admin/src/App.tsx index 3802189..11e20e8 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -2,6 +2,7 @@ import { Navigate, Outlet, Route, Routes } from 'react-router-dom' import { getToken } from '@/api/client' import { AdminLayout } from '@/components/admin-layout' import LoginPage from '@/pages/login' +import DashboardPage from '@/pages/dashboard' import PostsListPage from '@/pages/posts-list' import PostEditPage from '@/pages/post-edit' @@ -16,7 +17,8 @@ export default function App() { } /> }> }> - } /> + } /> + } /> } /> } /> diff --git a/admin/src/api/posts.ts b/admin/src/api/posts.ts index c431d0a..7073117 100644 --- a/admin/src/api/posts.ts +++ b/admin/src/api/posts.ts @@ -61,3 +61,14 @@ export function updatePost(id: string, form: PostForm) { export function deletePost(id: string) { return request(`/api/admin/posts/${id}`, { method: 'DELETE' }) } + +export interface Stats { + total: number + published: number + draft: number + recent: PostListItem[] +} + +export function fetchStats() { + return request('/api/admin/stats') +} diff --git a/admin/src/components/admin-layout.tsx b/admin/src/components/admin-layout.tsx index 0b2007b..c5f538c 100644 --- a/admin/src/components/admin-layout.tsx +++ b/admin/src/components/admin-layout.tsx @@ -1,52 +1,37 @@ -import { Link, Outlet, useNavigate } from 'react-router-dom' -import { LogOut, Moon, Sun } from 'lucide-react' -import { logout } from '@/api/auth' -import { useTheme } from '@/components/theme-provider' +import { useState } from 'react' +import { Outlet } from 'react-router-dom' +import { Menu } from 'lucide-react' +import { AdminSidebar } from '@/components/admin-sidebar' import { LogoMark } from '@/components/logo' export function AdminLayout() { - const navigate = useNavigate() - const { resolved, setTheme } = useTheme() + const [open, setOpen] = useState(false) return ( -
-
-
- - - sundynix admin - -
- - -
-
-
-
- -
+
+ setOpen(false)} /> + +
+ {/* 移动端顶部条 */} +
+ + + sundynix{' '} + admin + +
+ +
+ +
+
) } diff --git a/admin/src/components/admin-sidebar.tsx b/admin/src/components/admin-sidebar.tsx new file mode 100644 index 0000000..dc5777d --- /dev/null +++ b/admin/src/components/admin-sidebar.tsx @@ -0,0 +1,109 @@ +import { Link, NavLink, useNavigate } from 'react-router-dom' +import { FileText, LayoutDashboard, LogOut, Moon, Sun, X } from 'lucide-react' +import { logout } from '@/api/auth' +import { useTheme } from '@/components/theme-provider' +import { LogoMark } from '@/components/logo' +import { cn } from '@/lib/utils' + +const NAV = [ + { to: '/', label: '仪表盘', icon: LayoutDashboard, end: true }, + { to: '/posts', label: '文章', icon: FileText, end: false }, +] + +interface Props { + open: boolean + onClose: () => void +} + +export function AdminSidebar({ open, onClose }: Props) { + const navigate = useNavigate() + const { resolved, setTheme } = useTheme() + + const itemCls = ({ isActive }: { isActive: boolean }) => + cn( + 'flex items-center gap-3 rounded-lg px-3 py-2.5 text-[14px] transition-colors', + isActive + ? 'bg-accent-soft font-medium text-accent-ink' + : 'text-ink-2 hover:bg-ground hover:text-ink', + ) + + const bottomBtnCls = + 'flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-[14px] text-ink-2 transition-colors hover:bg-ground hover:text-ink' + + return ( + <> + {open && ( +