From 5052bcd8058a1ef9962755a0cff18a4c0b2d6d27 Mon Sep 17 00:00:00 2001 From: freefire Date: Wed, 16 Sep 2026 07:27:45 +0800 Subject: [PATCH] =?UTF-8?q?chore(dev):=20=E6=8C=89=E7=AB=AF=E5=8F=A3?= =?UTF-8?q?=E6=9D=80=E8=BF=9B=E7=A8=8B=E6=A0=91=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=20Windows=20=E5=AD=A4=E5=84=BF=E7=9B=91=E5=90=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 stop.bat/ps1/sh,并写入本地开发规则:停服务用脚本、停后复核端口。 Co-authored-by: Cursor --- .cursor/project_memory.md | 1 + .cursor/rules/jiang13-bbs-local-dev.mdc | 29 ++++++++++++++++- start.bat | 3 +- start.sh | 3 +- stop.bat | 21 +++++++++++++ stop.ps1 | 24 ++++++++++++++ stop.sh | 42 +++++++++++++++++++++++++ 7 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 stop.bat create mode 100644 stop.ps1 create mode 100644 stop.sh diff --git a/.cursor/project_memory.md b/.cursor/project_memory.md index c67f7aa..93b83a7 100644 --- a/.cursor/project_memory.md +++ b/.cursor/project_memory.md @@ -284,6 +284,7 @@ - **全站图片策略:用户上传的图片统一 WebP**。头像裁剪/转码在浏览器端完成(Canvas → toBlob('image/webp')),后端只收 WebP - 存储:`backend/data/uploads/avatars/{随机hex32}.webp`(已在 backend/data/ gitignore 内),Gin `r.Static("/uploads", ...)` 提供静态服务;Next rewrite 新增 `/uploads/:path*` 同源代理到后端(与 /api 一样走 BACKEND_URL 兜底 localhost:3001) - **本地开发数据目录铁律(2026-09-16)**:`config.Parse` 从 cwd 向上解析 backend 根,不跟 `os.Getwd()` 走。规范启动 `cd backend && go run ./cmd/jiang13`;数据只有 `backend/data/`。在 `cmd/jiang13/` 启动会写出另一套 `cmd/jiang13/data`,DB 里头像 404。规则见 `.cursor/rules/jiang13-bbs-local-dev.mdc`;可用 `JIANG13_WORK_PATH` 覆盖。启动日志打印 `workPath`/`dataDir`。`:3001` 已占用则不要再开第二个后端 +- **本地进程停干净(2026-09-16)**:Windows 上停 Cursor 终端常留下 `go`/`node` 孤儿听端口。用户用仓库根 `stop.bat`(Unix:`./stop.sh`)按 `:3000`/`:3001` + `taskkill /T` 杀进程树;agent 禁止只杀 shell、停后须复核端口空闲。详见 `jiang13-bbs-local-dev.mdc` - DB:`attachments` 表(user_id+kind 联合索引,kind 取值 `avatar`/`image`,记录 url/mime/size/width/height);`service/upload.go` 校验 RIFF/WEBP 魔数 + `golang.org/x/image/webp` 解码尺寸(正方形 64~512px)+ **大小限制以裁剪后文件为准(≤2MiB,MaxBytesReader 限请求体)**,落盘与 user.avatar 更新在同一事务 - 路由(需登录 + CSRF):`POST /api/upload/avatar`(multipart 字段 file)、`PUT /api/avatar/use`(只能选用本人 attachments 中的 url)、`GET /api/my/media`(本人全部附件,id DESC 限 60 条)、`DELETE /api/my/attachments/:id`(被帖子 content 引用返回 409;是当前头像则事务内置空 user.avatar;成功后物理删除文件) - **头像只能走上传/媒体库选用通道**:`UpdateProfile` 已移除 avatar 字段,禁止通过资料接口写任意外链 URL diff --git a/.cursor/rules/jiang13-bbs-local-dev.mdc b/.cursor/rules/jiang13-bbs-local-dev.mdc index 2adac2e..83072fd 100644 --- a/.cursor/rules/jiang13-bbs-local-dev.mdc +++ b/.cursor/rules/jiang13-bbs-local-dev.mdc @@ -1,5 +1,5 @@ --- -description: 本地开发启动约定:后端工作目录、唯一数据目录、单实例,避免头像/上传 404 +description: 本地开发启动/停止约定:工作目录、单实例、按端口杀进程树,避免孤儿进程与头像 404 alwaysApply: true --- @@ -30,6 +30,33 @@ npm.cmd run dev `:3000` 把 `/uploads` rewrite 到 `:3001`。头像 404 先查后端 `dataDir`,不要改前端。 +## 进程生命周期(Windows 孤儿进程) + +根因:Cursor/agent 停掉的往往是 **shell 父进程**;`go run` / `npm` 拉起的 `go.exe`/`node.exe`/编译产物仍在听端口,任务管理器里才看得见。 + +### Agent 必须遵守 + +1. **先查再启**:启动前用端口探测;`:3000`/`:3001` 已在听 → **复用**,禁止再开一份 +2. **停服务优先跑脚本**(仓库根): + - Windows:`stop.bat`(内部调用 `stop.ps1`) + - Unix:`./stop.sh` +3. **禁止**只依赖「结束终端 / Ctrl+C / 杀 shell PID」当作已停干净 +4. 无脚本时,Windows 用 **按端口 + 进程树** 清理(PowerShell): + ```powershell + foreach ($port in 3000,3001) { + Get-NetTCPConnection -LocalPort $port -State Listen -EA SilentlyContinue | + Select-Object -ExpandProperty OwningProcess -Unique | + ForEach-Object { if ($_ -gt 0) { taskkill /T /F /PID $_ } } + } + ``` +5. 声称已停止后,**再查一次**端口是否空闲;仍占用则继续 `/T` 杀树,勿让用户去任务管理器 +6. 默认**不动** Docker Postgres;用户要停库再 `docker compose down` +7. 会话结束前若本会话启动过前后端:主动清理或明确告知用户执行 `stop.bat` + +### 用户侧 + +双击 / 运行仓库根 `stop.bat`(或 `./stop.sh`)即可,无需开任务管理器。 + ## 遗留目录 `backend/cmd/jiang13/data/` 是错误 cwd 的产物,禁止当数据源。发现后把 `uploads/` 合并进 `backend/data/uploads/`,再删遗留目录。 diff --git a/start.bat b/start.bat index 3002d63..e9433c7 100644 --- a/start.bat +++ b/start.bat @@ -57,7 +57,8 @@ echo Backend: http://localhost:3001 echo Data dir: %~dp0backend\data echo Database: localhost:5432 echo. -echo Close each window to stop the service. +echo Prefer stop: stop.bat (kills :3000/:3001 process trees) +echo Or close each service window. echo Stop database: docker compose down echo ============================================ pause diff --git a/start.sh b/start.sh index bf77abf..8709326 100644 --- a/start.sh +++ b/start.sh @@ -74,7 +74,8 @@ echo " 数据库: localhost:5432" echo "" echo " 后端 PID: $BACKEND_PID" echo " 前端 PID: $FRONTEND_PID" -echo " 停止服务: kill $BACKEND_PID $FRONTEND_PID && docker compose down" +echo " 停止服务: ./stop.sh (或 Ctrl+C 本脚本)" +echo " 停库: docker compose down" echo "══════════════════════════════════════════" # 捕获 Ctrl+C,清理子进程 diff --git a/stop.bat b/stop.bat new file mode 100644 index 0000000..3afa70c --- /dev/null +++ b/stop.bat @@ -0,0 +1,21 @@ +@echo off +title jiang13-bbs stopper +echo ============================================ +echo jiang13-bbs Dev Stopper +echo ============================================ +echo. +echo 按端口终止监听进程树(:3000 前端 / :3001 后端)... +echo. + +REM 仅关 Cursor 终端 / Ctrl+C 往往杀不掉 go run / node 的子进程; +REM 此处按 Listen 端口取 OwningProcess,再 taskkill /T 杀整棵树。 +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0stop.ps1" +if errorlevel 1 ( + echo [WARN] stop.ps1 执行异常,可手动:taskkill /T /F /PID ^ +) + +echo. +echo [OK] 端口清理完成。 +echo PostgreSQL(Docker)未动;若要停库:docker compose down +echo ============================================ +pause diff --git a/stop.ps1 b/stop.ps1 new file mode 100644 index 0000000..b8989c4 --- /dev/null +++ b/stop.ps1 @@ -0,0 +1,24 @@ +# 按 Listen 端口终止进程树(含子进程)。默认 :3000 前端 / :3001 后端。 +# 由 stop.bat 调用;也可直接:powershell -File stop.ps1 + +param( + [int[]]$Ports = @(3000, 3001) +) + +$ErrorActionPreference = 'SilentlyContinue' + +foreach ($port in $Ports) { + $pids = @( + Get-NetTCPConnection -LocalPort $port -State Listen | + Select-Object -ExpandProperty OwningProcess -Unique + ) + if (-not $pids -or $pids.Count -eq 0) { + Write-Host " :$port 空闲" + continue + } + foreach ($procId in $pids) { + if ($procId -le 0) { continue } + Write-Host " :$port -> PID $procId (含子进程)" + & taskkill.exe /T /F /PID $procId | Out-Null + } +} diff --git a/stop.sh b/stop.sh new file mode 100644 index 0000000..d01614b --- /dev/null +++ b/stop.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 按端口终止 :3000 / :3001 的监听进程(含子进程) +# 用法:./stop.sh + +ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$ROOT_DIR" + +kill_port() { + local port="$1" + local pids + if command -v lsof &>/dev/null; then + pids="$(lsof -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null || true)" + elif command -v fuser &>/dev/null; then + fuser -k "${port}/tcp" 2>/dev/null || true + echo " :${port} 已尝试 fuser -k" + return + else + echo " 需要 lsof 或 fuser 才能按端口清理" + return 1 + fi + if [ -z "${pids}" ]; then + echo " :${port} 空闲" + return + fi + # shellcheck disable=SC2086 + echo " :${port} -> PID ${pids}" + kill ${pids} 2>/dev/null || true + sleep 0.3 + # shellcheck disable=SC2086 + kill -9 ${pids} 2>/dev/null || true +} + +echo "══════════════════════════════════════════" +echo " jiang13-bbs Dev Stopper" +echo "══════════════════════════════════════════" +echo "按端口终止 :3000 / :3001 ..." +kill_port 3000 +kill_port 3001 +echo "完成。停库请另执行: docker compose down" +echo "══════════════════════════════════════════"