# 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 特性,先查官方文档再生成代码 7. 用户未明确告知「已正式部署到生产环境」之前:禁止为旧版本/旧契约写兼容冗余(双写双读、fallback、过渡字段、旧 cookie/旧 API 垫片);直接改到位并删旧代码,保持源码干净规范 --- ## 技术栈 - 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 列 - 用户安全设置「最近登录设备」:GET /api/me/login-devices(RequireAuth,仅本人仍有效的 refresh 会话,按归一化 IP+UA 指纹去重);DELETE /api/me/login-devices/:id 剔除其它设备(吊销该指纹下除当前会话外的 refresh,JWT `fid` 使 access 立即失效,需重新输入密码)。展示设备/浏览器与原始 IP(不使用 ip2region);本机标 current=当前在线,其余有效会话标在线。SSR 转发 refresh cookie + 浏览器 UA/IP,直出 `/u/:id?tab=security` - 角色变更事务内 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 审核队列页(待审角标 WS `moderation:changed` + 回前台 HTTP 校准,无定时轮询);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=/`(供 Next middleware 在页面请求中续期),**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 Bearer) - `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,ParseToken+ValidateClaims 双校验失败 401 JSON;checkWSOrigin:**必须带 Origin**,dev 允许 localhost/127.0.0.1 任意端口(页面 :3000 直连 :3001),生产要求 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 `