From 11548eec9771f80654424e0ee67093f2eb4c7a9d Mon Sep 17 00:00:00 2001 From: freefire Date: Wed, 16 Sep 2026 00:25:34 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E7=BA=B3=E5=85=A5=20Cursor=20?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E8=A7=84=E5=88=99=E5=B9=B6=E5=BF=BD=E7=95=A5?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E8=BF=90=E8=A1=8C=E6=97=B6=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充 .cursor 规则/技能与记忆;gitignore 排除 backend/cmd/jiang13/data 与调试日志。 Co-authored-by: Cursor --- .cursor/project_memory.md | 289 ++++++++++++++++++ .cursor/rules/jiang13-bbs-auth-realtime.mdc | 28 ++ .cursor/rules/jiang13-bbs-core.mdc | 43 +++ .cursor/rules/jiang13-bbs-engineering.mdc | 29 ++ .../skills/theme-recolor-workflow/SKILL.md | 45 +++ .cursor/user_preferences.md | 8 + .gitignore | 2 + 7 files changed, 444 insertions(+) create mode 100644 .cursor/project_memory.md create mode 100644 .cursor/rules/jiang13-bbs-auth-realtime.mdc create mode 100644 .cursor/rules/jiang13-bbs-core.mdc create mode 100644 .cursor/rules/jiang13-bbs-engineering.mdc create mode 100644 .cursor/skills/theme-recolor-workflow/SKILL.md create mode 100644 .cursor/user_preferences.md diff --git a/.cursor/project_memory.md b/.cursor/project_memory.md new file mode 100644 index 0000000..6d8c77f --- /dev/null +++ b/.cursor/project_memory.md @@ -0,0 +1,289 @@ +# jiang13-bbs 论坛项目记忆 + +> 来源:Trae CN `~/.trae-cn/memory/.../project_memory.md`,于 2026-09-15 导入 Cursor(`.cursor/`)。 +> 配套:`user_preferences.md`、`skills/theme-recolor-workflow/`、以及 `rules/jiang13-bbs-*.mdc`(alwaysApply)。 + +## 记忆初始化指令 + +以下为项目核心规则,所有后续开发必须遵守: + +1. 项目使用 Go 1.27 + Next.js 16,禁止生成旧版本惯用代码 +2. Next.js 16 的 params 等必须 await,这是最高频错误,每次生成后自查 +3. 如果部署环境涉及 Cloudflare Workers,middleware.ts 不能改名为 proxy.ts +4. 所有 SEO 关键内容必须在初始 HTML 中,不接受客户端注入 +5. 权限逻辑只在 Go 端,前端不做权限判定 +6. 遇到不确定的 Next.js 16 或 Go 1.27 特性,先查官方文档再生成代码 + +--- + +## 技术栈 + +- Go 1.27 + Gin +- Next.js 16 (App Router) + TypeScript + Tailwind CSS +- PostgreSQL + +--- + +## Next.js 16 关键约束 + +- params/searchParams/cookies()/headers()/draftMode() 必须 await +- 部署到 Cloudflare Workers 时保持 middleware.ts 不变(OpenNext 不识别 proxy.ts) +- 所有 @folder 必须有 default.tsx +- 使用 Metadata API,禁止 next/head +- 交互组件必须标记 'use client' +- next/image 默认 TTL 4 小时,需要时在 next.config.ts 覆盖 minimumCacheTTL +- Pure SSR website: skeleton screens should not be used; F5 hard refresh should directly display SSR content without skeleton screen flickering (app/loading.tsx and Skeleton components have been removed) + +--- + +## Go 1.27 关键约束 + +- 优先使用泛型方法替代重复方法定义 +- 使用 encoding/json/v2 而非旧版 json +- 使用标准库 uuid 而非第三方库 +- 结构体字面量支持嵌入字段直接初始化 + +--- + +## SEO 规则 + +- 所有公开页面 SSR/ISR +- SEO 关键内容(title、description、OG、JSON-LD)必须在初始 HTML 中 +- 帖子详情页输出 DiscussionForumPosting 类型的 JSON-LD +- sitemap.ts 和 robots.ts 必须存在 + +--- + +## 权限规则 + +- 权限校验在 Go 后端执行,前端不参与判定 +- 所有受保护 API 必须经过 RBAC 中间件 +- 速率限制:登录 20/分钟,注册 10/分钟,发帖新用户 24h 冷静期 +- 帖子编辑/删除权限:作者本人或管理员可执行,其他用户返回 403,未登录返回 401 +- 置顶/推荐操作仅管理员可执行,非管理员返回 403 + +--- + +## 四级 RBAC + 内容审核(2026-09) + +- 角色:`owner`(全站唯一,迁移把 id=1 testuser 升站长,唯一硬保护:不可改角色/封禁) > `super_admin`(全站后台,仅 owner 可授予) > `admin`(公告+全站帖/评论审核+置顶加精) > `board_admin`(仅 user_boards 授权板块帖/评论审核) > `user`。RoleLevel 0/30/50/80/100,staff=board_admin 及以上 +- Actor 由 RequireStaff 每请求从 DB 现取(角色+board_ids);RequirePerm(PermAnnouncements/PermUsers/PermSettings) 挂在 staffAPI 子组;板块隔离在 service 层按 Actor.BoardIDs +- 发帖/评论:staff 直发 published,普通用户进 pending;pending 不计 comment_count(ApproveComment 时 +1,删除只减 published);公开列表仅 published;作者/staff 可经详情页看 pending/rejected(GetByIDForViewer + EnsurePostVisible) +- 审核端点 7 个直挂 /api/admin/moderation/*;通过/拒绝发 approved/rejected 通知(拒绝需 reason,评论通过时补发 comment/reply 业务通知);ErrModerationForbidden→403、ErrNotPending→400 +- 登录历史 login_logs:成功失败都记录(失败用户名可不存在 user_id=0);GET /api/admin/users/:id/login-logs;用户列表有 online(last_seen_at 5 分钟内绿点)/last_login_ip/last_login_at 列 +- 角色变更事务内 invalidateUserSessions(token_version+1 + 撤销 refresh tokens),仅板块授权变化不下线;board_admin 授权必须 ≥1 个有效板块否则 400 +- 前端:lib/roles.ts(ROLE_META:owner=Crown/--gold,super=BadgeCheck,admin=ShieldCheck,board=Shield,user=Users;lucide 无 ShieldStar)、RoleBadge 组件(truncate 用于窄列+板块名);/admin/moderation 审核队列页(30s 角标轮询仅入口可见时);Forbidden.tsx 子页守卫视图(HTTP 仍 200);用户管理 /admin/users 首列收窄 grid-cols-[minmax(0,1fr)_78px_...] +- 新用户发帖有 24h 冷静期(注册接口无此限制,API 回归需 SQL 回退 created_at);CreatePostRequest.tags 是字符串不是数组 + +--- + +## 认证方案(HttpOnly Cookie + CSRF + Refresh Token + Token Version) + +**不使用 localStorage 存储 token**(XSS 可窃取),采用三 Cookie + Token Version 方案: + +- `j13_token`:access token JWT,`HttpOnly + SameSite=Lax`,**15 分钟**过期,JS 不可读 +- `j13_refresh`:refresh token(DB 存储),`HttpOnly + SameSite=Lax`,`Path=/`(2026-09 起从 /api/auth 放宽,供 Next middleware 在页面请求中续期;clearAuthCookies 同时清 / 与 /api/auth 兼容旧 cookie),**7 天**过期 +- `j13_csrf`:CSRF token,非 HttpOnly(JS 可读),`SameSite=Lax`,7 天过期 + +**后端**(`service/auth.go`、`middleware/auth.go`、`middleware/csrf.go`、`middleware/security.go`): +- `User.TokenVersion` 字段:改密码/封禁/管理员强制下线时递增,使所有 JWT 立即失效 +- `parseToken` 优先从 `j13_token` cookie 读取,回退到 Authorization header +- `ValidateClaims` 每请求查 DB:校验 `token_version` 匹配 + `banned` 状态(不依赖 JWT 缓存值) +- `CSRFMiddleware` 对非 GET 方法校验 `X-CSRF-Token` header 与 `j13_csrf` cookie 一致 +- 需登录 API 组挂载顺序:`RequireAuth → CSRFMiddleware`(未登录返回 401 而非 403) +- `RefreshToken` 表存储 refresh token 的 SHA-256 哈希,`RotateRefreshToken` 每次刷新撤销旧 token 签发新的(防重放),并设置宽限期内重放返回同一新 token +- 检测到已撤销 refresh token 重放时,立即 `RevokeAllUserRefreshTokens` 吊销该用户所有 refresh token +- `/api/auth/refresh` 端点验证 refresh cookie 后轮转签发新 access + refresh + csrf +- 登出递增 `User.TokenVersion` 使 access token 立即失效,同时撤销该用户所有 refresh token + 清除三 cookie +- `SecurityHeaders` 中间件全局添加:CSP、HSTS、X-Frame-Options: DENY、X-Content-Type-Options: nosniff、Referrer-Policy + +**前端**(`lib/api.ts`): +- 客户端请求使用相对路径 `/api/*`(走 Next.js rewrite 代理,浏览器视为同源) +- 所有 fetch 带 `credentials: 'include'` +- 状态变更请求(POST)自动从 `document.cookie` 读取 `j13_csrf` 并加入 `X-CSRF-Token` header +- `fetchWithRefresh`:遇到 401 自动调用 `/api/auth/refresh` 续期后重试(refresh cookie 浏览器自动携带) +- 登录态通过 `GET /api/me`(OptionalAuth)获取,前端不存储任何 token +- SSR 端公开接口(fetchPosts 等)仍直连 `API_BASE`,无需 cookie + +**SSR 登录态直出(2026-09 起,消除 F5 用户区闪动)**: +- `frontend/middleware.ts`:页面/RSC 请求前本地解码 j13_token 的 exp(不验签),过期且有 j13_refresh 时调后端 refresh 轮转,新 cookie 注入请求头供本次 SSR + 原样透传 Set-Cookie 给浏览器;同一 refresh token 有 10s 模块级去重(防 RSC 预取与导航并发导致重复轮转被判重用清登录态),仅缓存成功结果且定期清理过期条目;matcher 排除 /api/、_next/、静态资源;dev 环境保留 `http://localhost:3001` 兜底,生产构建强制要求显式配置 `BACKEND_URL`/`NEXT_PUBLIC_API_URL` +- `app/layout.tsx` 用 cookies() 调 `fetchMe`/`fetchUnreadCount`(lib/api.ts 的 SSR 函数,转发 cookie 直连 API_BASE,设置超时兜底),把 initialUser/initialUnread/initialTheme 传给 Header;Header 用 initial 值初始化 state,不再 useEffect 调 apiMe;props 随 router.refresh() 校正 +- 登录页 push 后必须 `router.refresh()`(root layout 在客户端导航中持久化);改密成功同理 +- NotificationBell 红点也由 initialUnread SSR 直出,挂载后靠 WS `notification:new` + `visibilitychange` HTTP 校准(**已取消 30s 定时轮询**) +- 字体用 next/font/google 构建期自托管(Plus Jakarta Sans / Noto Sans SC / JetBrains Mono,CSS 变量 --font-jakarta/--font-noto-sc/--font-jetbrains),禁止再引入 fonts.googleapis.com CDN link(会 FOUT"由粗变细"闪动) + +--- + +## WebSocket 实时总线(2026-09 一期) + +群聊前置基建:**全站一条 WS 复用所有实时事件**;角标/主题以推送为主,HTTP 仅作 visibility/focus 校准(**无定时轮询**)。 + +**实时化分期路线(用户 2026-09-15 冻结,跨会话必须延续,勿遗忘)**: +- 一期(已完成 2026-09-15):WS 总线 + 在线状态推送 + 管理员设置变更推送(本节详见) +- 二期(已完成 2026-09-15):**群聊** —— 建群 / 成员管理 / 消息持久化 / 未读数 / @提醒 / 历史分页。铁律:消息先落库再经 WS 广播,WS 不承担可靠投递;离线未读靠 HTTP 对账。详见下方"群聊二期"节 +- 三期(已完成 2026-09-15):帖子流「有新内容」提示 —— `feed:changed` 全员广播(staff 直发/审核通过帖评),首页与板块页 tip,点击 `router.refresh()`;不做自动插行/游客轮询 + + +- 端点 `GET /api/ws`(gorilla/websocket v1.5.3,挂在 r.Static 之后、pubAPI 之前,不走任何中间件组,鉴权在处理器内):cookie j13_token 优先、Bearer 回退,ParseToken+ValidateClaims 双校验失败 401 JSON;checkWSOrigin:无 Origin 放行,dev 允许 localhost/127.0.0.1 任意端口,生产要求 Origin host==Host +- 代码:`backend/realtime/hub.go`(Hub:clients/rooms/userConnCnt 多标签页计数;register 0→1 才上线广播,最后一条连接断开才下线广播;房间 staff、个人房 user:{id};dispatch 满缓冲 forceClose 慢消费者;OnlineUserIDs 快照)、`backend/realtime/client.go`(writePump 唯一写连接:30s 协议 Ping、70s pongWait、send buf 32;readPump 只认 {"type":"ping"} 应用帧,每次心跳复查 ValidateClaims+TouchLastSeen(60s SQL 限频),失败写 **关闭码 4401**)、`handler/realtime.go`;Handlers.Hub 在 router.NewHub() +- 信封单层 JSON `{type,data}`(encoding/json/v2);事件:`hello`(staff 的 data 带 online_user_ids 在线快照)、`pong`、`settings:changed`{accent}(BroadcastAll)、`presence:update`{user_id,online}(只 BroadcastRoom staff);UpdateSettings 保存后广播,accent 为归一化小写 trim 值 +- **在线判定新口径:有活跃 WS 即在线**(取代 last_seen_at 5 分钟启发式,更精确;用户管理页绿点由 presence 推送+hello 快照 reconcile,SSR initial.online 仍走旧口径) +- 封禁/改角色使 token_version 不符后,WS 在下次心跳(前端 25s ping,≤25s)收到 4401;前端收到 4401 不重连,交给 HTTP 拦截/封禁模态 +- 前端 `lib/realtime.ts` 单例 `realtime`:状态机 idle/connecting/open/closed,指数退避 1→15s ±20% 抖动,window online/visibilitychange 触发重连,25s 心跳;`on/off` 订阅、onPresence、getOnlineIds 快照;`RealtimeProvider`(根布局 body 末尾,按 userId connect/disconnect,null 不连) +- **dev 直连 `ws://hostname:3001/api/ws`**(Next dev rewrite 不透传 WS Upgrade;cookie 不按端口隔离所以同源 cookie 自动带);`NEXT_PUBLIC_WS_URL` 可覆盖;生产同源 /api/ws,Nginx 需配 Upgrade/Connection 头;Cloudflare Workers 不能透传任意 WS +- 已接入:ThemeSync 订阅 settings:changed 无刷新热换肤(推送优先;**已取消 60s 定时轮询**,仅 visibility/focus + pathname 变化时 HTTP 校准;/admin/appearance 下整个 effect 暂停);AdminNav 待审角标订阅 `moderation:changed`(staff 房间)后 HTTP 校准(**已取消 30s 定时轮询**);/admin/users 在线点实时跳变 +- **主题换肤 DOM 铁律(2026-09-15)**:SSR `