chore: 纳入 Cursor 项目规则并忽略本地运行时数据
补充 .cursor 规则/技能与记忆;gitignore 排除 backend/cmd/jiang13/data 与调试日志。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
289
.cursor/project_memory.md
Normal file
289
.cursor/project_memory.md
Normal file
@@ -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 `<style id="j13-accent-vars">` 由 React 托管,客户端 **禁止** 对其 `remove()`/`改写`(否则 React reconcile 报 removeChild);热换肤只操作独立节点 `#j13-accent-runtime`(挂 body 末尾压过 SSR)。恢复默认色时:若 SSR 仍有自定义内容则 runtime 写入默认令牌覆盖,否则卸掉 runtime
|
||||||
|
- 实测(2026-09-15):未握手 401、hello/pong/心跳、settings 双端推送热换肤与恢复默认、上下线 presence、封禁后 4401 踢出;通知/待审角标改推送后日志不再刷 30s HTTP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 帖子流「有新内容」三期(2026-09-15 完成)
|
||||||
|
|
||||||
|
- 事件 `feed:changed`:`{kind:"post"|"comment", post_id, board_id, actor_id}`,`BroadcastAll`
|
||||||
|
- 触发:`CreatePost`/`CreateComment` 仅 published;`ApprovePost`/`ApproveComment` 成功后广播(reject 不推)
|
||||||
|
- 前端 `FeedNewTip`:首页(非搜索)与 `/board/[id]`;page=1;`sort=new` 仅 post;`latest` 接受 post+comment;`recommended` 忽略;`actor_id===userId` 忽略;点击 `router.refresh()` + 滚顶
|
||||||
|
- 不做自动插行、不做游客轮询
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 群聊二期(2026-09-15 完成)
|
||||||
|
|
||||||
|
**模型**(`model/models.go`):
|
||||||
|
- `ChatRoom`:id/name/description/owner_id/member_count/last_message_id + soft delete;`Owner User`(值类型 FK)、`LastMessage *ChatMessage`(指针 FK,**必须 `constraint:-`** 防止 GORM 建 DB 级 FK 约束——last_message_id 默认 0 而 chat_messages 无 id=0 行)
|
||||||
|
- `ChatRoomMember`:room_id+user_id 联合唯一索引、role(owner/member)、last_read_message_id(未读水位)、muted;`User User` 值类型 FK
|
||||||
|
- `ChatMessage`:room_id+created_at 复合索引、sender_id、content(varchar 2000)、mention_ids(逗号分隔 user_id,落通知用) + soft delete;`Sender User` 值类型 FK
|
||||||
|
- `Notification`:新增 `RoomID uint`(index,not null,default:0)、`MessageID uint`(not null,default:0) 字段;`Room *ChatRoom`(指针 FK,**必须 `constraint:-`**——同上,room_id=0 违反 FK);Preload Room 在通知列表中
|
||||||
|
|
||||||
|
**GORM FK 约束陷阱(2026-09 首次迁移 bug)**:
|
||||||
|
- GORM AutoMigrate 对**指针类型关联**(`*ChatRoom`/`*ChatMessage`)自动创建 DB 级 FK 约束,对**值类型关联**(`User`/`Post`)不创建
|
||||||
|
- `Notification.Room *ChatRoom` 和 `ChatRoom.LastMessage *ChatMessage` 的 FK 约束在首次迁移时添加失败(notifications 表已有数据 room_id=0;chat_rooms 新建但 last_message_id=0)
|
||||||
|
- 修复:模型加 `constraint:-` 标签防止新建 + `db.go` 的 `dropStaleChatFKConstraints` 函数在 AutoMigrate 前清理遗留约束(幂等 DROP CONSTRAINT IF EXISTS)
|
||||||
|
|
||||||
|
**后端 Service**(`service/chat.go`):
|
||||||
|
- 建群:创建 ChatRoom + ChatRoomMember(owner),事务;群名 1-64 字符,描述 ≤256
|
||||||
|
- 加入/退出/解散:member_count 实时维护;解散仅 owner,软删 room + 硬删 members;退出自动离开 WS 房间
|
||||||
|
- 发消息:content 1-2000 字符,30 条/分钟限流;先落库 → 更新 room.last_message_id → 解析 @username 验证群成员 → 批量 CreateMention 通知 → WS 广播 chat:message
|
||||||
|
- 历史分页:cursor 分页(before_id),每页 50 条,按 id ASC 返回;成员才能看历史
|
||||||
|
- 未读数:`ChatUnreadSummary` 按 room_id GROUP BY 统计 `id > last_read_message_id` 的消息数;`MarkChatRead` 更新水位
|
||||||
|
- 成员管理:列出成员(按 role DESC, created_at ASC);踢人仅 owner,事务内删除 member + WS RemoveUserRoom + 广播 chat:membership
|
||||||
|
|
||||||
|
**后端 WS 扩展**(`realtime/hub.go`):
|
||||||
|
- 新增房间 `chat:{roomID}`,`joinRoom`/`leaveRoom` 帧鉴权(查 ChatRoomMember 存在性)
|
||||||
|
- 事件:`chat:message`{id,room_id,sender_id,content,created_at,sender}(BroadcastRoom chat:{roomID})、`chat:membership`{room_id,user_id,action}(BroadcastRoom)、`notification:new`{id}(单播 user:{id})
|
||||||
|
- 踢人:`RemoveUserRoom` 立即从 chat:{roomID} 移除连接
|
||||||
|
- 重连自动重订:前端 `joinRoom` 调用幂等,WS onopen 后自动重发 join 帧
|
||||||
|
|
||||||
|
**后端 Handler/Router**(`handler/chat.go`、`router/router.go`):
|
||||||
|
- 路由组 `/api/chat`(RequireAuth + CSRF):
|
||||||
|
- `GET /rooms`(分页+搜索,返回 ChatRoomView 含 joined/my_role/unread_count)
|
||||||
|
- `POST /rooms`(建群)、`GET /rooms/:id`(详情)、`PUT /rooms/:id`(改名/描述,owner)、`DELETE /rooms/:id`(解散,owner)
|
||||||
|
- `POST /rooms/:id/join`、`POST /rooms/:id/leave`
|
||||||
|
- `GET /rooms/:id/members`、`DELETE /rooms/:id/members/:uid`(踢人,owner)
|
||||||
|
- `GET /rooms/:id/messages`(历史分页)、`POST /rooms/:id/messages`(发消息,30/min 限流)
|
||||||
|
- `PUT /rooms/:id/read`(标记已读)、`GET /unread-summary`(各群未读汇总)
|
||||||
|
- `/api/me` 响应新增 `chat_unread_count`(全群未读总数)
|
||||||
|
|
||||||
|
**前端**:
|
||||||
|
- `lib/api.ts`:`ChatRoom`/`ChatRoomView`/`ChatMessage`/`ChatRoomMember` 接口 + api 函数(fetchChatRooms/fetchChatRoom/apiCreateChatRoom/apiSendChatMessage/apiListChatMessages/apiMarkChatRead/apiListChatMembers/apiLeaveChatRoom/apiDissolveChatRoom/apiUpdateChatRoom/apiKickChatMember/apiChatUnreadSummary)
|
||||||
|
- `lib/realtime.ts`:新增 `RT_CHAT_MESSAGE`/`RT_CHAT_MEMBERSHIP`/`RT_NOTIFICATION_NEW` 常量 + `RtChatMessageData`/`RtChatMembershipData` 接口;`joinRoom`/`leaveRoom`/`clearRooms` 方法;重连后自动重订已加入房间
|
||||||
|
- `app/chat/page.tsx`:SSR 群列表 + 搜索 + 分页 + 建群弹窗(client component)
|
||||||
|
- `app/chat/[id]/page.tsx`:SSR 群详情 + 初始 30 条消息 + ChatRoomClient
|
||||||
|
- `app/chat/[id]/ChatRoomClient.tsx`:消息收发(WS 实时 + HTTP 发送)、历史分页向上加载、@高亮(群成员验证)、成员面板、设置面板(owner 改名/解散)、踢人、未读标记、自动滚底
|
||||||
|
- `components/Header.tsx`:群聊图标入口(MessageSquare)+ 未读红点(chat_unread_count SSR 直出 + WS chat:message 实时递增,当前页不计数)
|
||||||
|
- `components/NotificationBell.tsx`:订阅 `RT_NOTIFICATION_NEW` 实时递增未读 + 刷新列表
|
||||||
|
|
||||||
|
**实测(2026-09-15)**:建群、消息收发、列表/详情 SSR、Header 未读红点全部通过;go build/vet/gofmt + next build 20 路由全过
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git / 仓库规则
|
||||||
|
|
||||||
|
- 远端:`origin = https://git.iioio.com/freefire/jiang13-bbs.git`,主分支 `main`
|
||||||
|
- **push 重试规则(用户指定)**:远端是自建 Gitea(偶发不稳)。`git push` 首次失败时**原样再试一次**即可;不要反复重试、不要改 remote / 切换 SSH·HTTPS
|
||||||
|
- 首次提交已重做:`node_modules/`、`.next/`、`backend/app.ini`、`backend/data/`(含 `.jwt_secret`)、`.trae/` 均已加入 `.gitignore`,禁止入库
|
||||||
|
- 配置模板提交 `*.example`(`.env.example`、`backend/app.ini.example`),真实配置文件不提交
|
||||||
|
- 许可证:**专有许可证**(LICENSE,Copyright © 2026 姜十三,保留所有权利)。用户明确不允许他人免费使用/商用,源码仅可见;不要替换为 MIT/Apache/GPL 等开源协议
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前端主题与配色规则
|
||||||
|
|
||||||
|
- 配色修改仅允许改动 frontend/app/globals.css 中的 CSS 令牌,禁止修改组件代码
|
||||||
|
- 明暗主题配色必须成组成对替换,确保一致性
|
||||||
|
- 所有文字内容需满足 WCAG 2.1 AA 标准(对比度 ≥4.5:1),交互控件边界/焦点对比度 ≥3:1
|
||||||
|
- 使用 Node 脚本核算对比度并生成截图回归验证(evidence/ 目录归档)
|
||||||
|
- **后台主题色实时下发(2026-09)**:theme_accent SSR 内联在根布局 `#j13-accent-vars`;客户端换肤走 `#j13-accent-runtime`(见上「主题换肤 DOM 铁律」)。ThemeSync:WS `settings:changed` 优先,visibility/focus(10s 限频)HTTP 校准,**无定时轮询**;`/admin/appearance` 暂停同步避免覆盖未保存预览
|
||||||
|
- **站长用户内容档案(2026-09-15)**:仅 `owner` 可访问 `GET /api/admin/users/:id` 及 posts/comments/messages(Unscoped 含驳回/软删/撤回标识);前端 `/admin/users/[id]`;撤回消息**保留正文**;普通成员居中系统提示(本人 2 分钟内可「重新编辑」);站长/超管单气泡「已撤回」+ 原文(早期清空库内正文的记录无法还原)
|
||||||
|
- **管理仪表盘**:去掉与前台同源的 4 站点统计卡与快捷大卡;保留待办审核区 + 最新注册 + 板块分布
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hard Constraints
|
||||||
|
- Main comments count as floors; sub-comments do not count as floors
|
||||||
|
- Comment depth is limited to 10 levels
|
||||||
|
- 帖子详情页三栏宽度与首页统一,容器同为 max-w-[1440px]、间距同为 gap-6 / xl:gap-7
|
||||||
|
- 板块卡片与帖子列表背景色必须保持一致(浅色均 #fff,暗色均 rgb(24,27,33))
|
||||||
|
- 禁止操作自己的管理员账号;至少保留一名未封禁管理员
|
||||||
|
- 角色参数必须使用白名单验证
|
||||||
|
- 封禁与管理员降级操作需在同一事务内递增 `token_version` 并撤销 refresh tokens,使旧 JWT 立即失效
|
||||||
|
- `/api/logout` 端点必须移出 RequireAuth 中间件,确保被封禁用户能清除 HttpOnly cookie
|
||||||
|
- 封禁状态在后端持久化,所有认证入口(登录/refresh token/受保护 API/切回标签页)需实时校验并返回 `code: "account_banned"`
|
||||||
|
- 前端使用单一强制下线事件总线统一感知封禁状态,实现状态清理与用户告知解耦
|
||||||
|
- 管理员操作确认弹窗必须使用站点化设计(fixed inset-0 z-[1000] 遮罩 + panel 居中卡片),禁止使用原生 window.confirm
|
||||||
|
- 管理员操作确认弹窗执行中需锁定 ESC/遮罩关闭,失败时保留弹窗并 toast 报错
|
||||||
|
- 封禁告知模态需挂载在 root layout,确保任意页面都能弹出且不可误关
|
||||||
|
|
||||||
|
## Engineering Conventions
|
||||||
|
- Comment model includes `parent_id`, `root_id`, and `depth` columns
|
||||||
|
- `ListFloorPaged` service assembles comment trees in memory with a single query to avoid N+1 issues
|
||||||
|
- Main comments display floor numbers (continuous across pages); sub-comments and nested replies use indented tree display
|
||||||
|
- Reply forms appear inline below target comments; quote replies use the format `> @Author #Floor`
|
||||||
|
- 帖子详情页 Grid 轨道:lg:grid-cols-[224px_minmax(0,1fr)_280px]、xl:grid-cols-[240px_minmax(0,1fr)_300px],lg 起即为三栏
|
||||||
|
- 帖子详情页左栏 aside 使用 hidden lg:flex w-56 xl:w-60(与首页 WorkspaceNav 同宽同断点)
|
||||||
|
- 帖子详情页右栏 aside 使用 w-[280px] xl:w-[300px](与首页 CommunityPanels 一致)
|
||||||
|
- 帖子详情页正文 article 去掉 max-w-[760px] mx-auto,使其占满剩余轨道
|
||||||
|
- 帖子详情页窄屏作者卡跟随左栏断点由 xl:hidden 改为 lg:hidden,避免重复展示
|
||||||
|
- 板块卡片选中态通过左侧 3×20px 圆角竖条和标题文字变 accent 色表达,不使用背景色区分
|
||||||
|
- 管理员管理面板路由为 `/admin/*`,鉴权与 `noindex` 由 `app/admin/layout.tsx` 统一处理
|
||||||
|
- 管理员侧边栏导航在 `app/admin/AdminNav.tsx` 中维护,新增模块通过修改 `NAV_ITEMS` 实现
|
||||||
|
- 用户管理列表支持分页(默认 20,上限 50)、用户名/昵称/邮箱 ILIKE 搜索、角色与状态筛选
|
||||||
|
- 用户管理操作后自动重新拉取数据以保证计数一致性;查询状态同步到 URL 参数(`?q=&filter=&page=`)
|
||||||
|
- 管理员 API 路由需依次经过 RequireAuth → CSRF → RequireAdmin 中间件
|
||||||
|
- 用户管理列表展示自己账号时仅显示"当前账号"标识,无操作按钮
|
||||||
|
- 危险操作(如封禁、降级)需显示明确的 confirm 提示文案
|
||||||
|
- 移动端用户管理列表使用卡片布局,桌面端使用七列表格布局
|
||||||
|
- 管理员仪表盘快捷入口包含用户管理、公告管理、外观设置
|
||||||
|
- 用户管理汇总卡显示总数(含今日新增)、管理员数量、已封禁数量及当前筛选结果数量
|
||||||
|
- 搜索框使用 400ms 防抖处理
|
||||||
|
- 管理员操作 API 错误码统一映射为 400/404
|
||||||
|
- 用户管理列表分页控件使用上一页/下一页按钮
|
||||||
|
- 帖子/评论计数通过 GROUP BY 批量填充,避免 N+1 查询问题
|
||||||
|
- 汇总卡数据不受筛选条件影响
|
||||||
|
- 管理员操作确认弹窗需区分四种操作独立文案与色调:提权(accent 盾牌)、降级(danger 盾叉,提示立即下线)、封禁(danger Ban)、解封(ok 旋转箭头)
|
||||||
|
- 前端事件总线(userEvents.ts)需支持 `j13:force-logout` 事件,实现全局封禁状态感知
|
||||||
|
- API 拦截器(api.ts)需在普通请求、401 刷新重试、refresh、/me 四处统一拦截封禁状态并去重派发事件
|
||||||
|
- NotificationBell 组件需增加 `visibilitychange` 监听,确保切回标签页时立即探测封禁状态
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
- Deleting a comment cascades soft deletion of the entire subtree
|
||||||
|
- **GORM bool 列零值陷阱(2026-09 登录日志 bug)**:`Success bool gorm:"default:true"` 时插入 false 会被 GORM 当零值省略,DB 落 default true,失败记录被错存成成功。规则:布尔列默认值必须是零值语义(default:false),写入时用 `db.Select(全部列).Create()` 强制写;AutoMigrate 不会改列默认值,需在 db.go 加幂等 `ALTER TABLE ... ALTER COLUMN ... SET DEFAULT`(参考 login_logs.success)
|
||||||
|
- **待审内容 SSR 必须转发 cookie**:pending/rejected 帖的详情与评论接口对游客 404,帖子详情页 SSR 所有数据拉取(含 generateMetadata、fetchComments)都要带 authCookieHeader,否则作者提交后跳转看到 500/"帖子不存在";404 用 next/navigation 的 notFound() 落全局 not-found 页而非让页面抛 500
|
||||||
|
- Windows PowerShell 5.1 跑 .ps1 脚本:含中文的脚本文件必须存成 UTF-8 with BOM,否则按 ANSI 解析直接语法错误;中文断言匹配需用 UTF8 字节数组匹配,IWR .Content 会乱码;npx/npm 必须用 npx.cmd/npm.cmd
|
||||||
|
- 圆形元素需设置 `self-center justify-self-center w-full` 避免 Grid 布局中被拉伸成椭圆;内部铺满元素使用 `absolute inset-0` 而非 `h-full` 解决 `aspect-ratio` 尺寸解析歧义
|
||||||
|
- `<Avatar>` 所有调用点必须显式传 `src={xxx.avatar || undefined}`,否则默认渲染字母头像
|
||||||
|
- 搜索清除后应立即拉取数据,避免显示延迟
|
||||||
|
- 空筛选状态需显示正确的提示文案
|
||||||
|
- SSR 深链 `?filter=admin|banned` 应能正确直出筛选结果
|
||||||
|
- koa-connect wrapper caused ctx leaks when migrating Express middleware to Koa; native Koa rewrite is required instead of wrapping Express middleware
|
||||||
|
- 使用 koa-connect 包装 Express 中间件会导致 ctx.state 数据丢失,需采用原生 Koa 中间件重写
|
||||||
|
|
||||||
|
## 文件上传基础设施(2026-09 起)
|
||||||
|
|
||||||
|
- **全站图片策略:用户上传的图片统一 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)
|
||||||
|
- 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
|
||||||
|
- 前端:react-easy-crop v6(必须 `import "react-easy-crop/react-easy-crop.css"`),裁剪弹窗 `components/AvatarCropModal.tsx`(上传区 + 媒体库 6 列圆形网格:点任意图进裁剪,非当前图 hover 两阶段确认删除),可点击头像 `components/AvatarPicker.tsx`(**仅资料表单内一个入口,Hero 头像纯展示**;按钮必须显式 width/height + inline-flex + line-height:0,否则 inline 基线 strut 致 80×86 椭圆),输出固定 320×320、质量 0.92→0.65 自适应直到 ≤2MB。**媒体库网格圆角光环另有一坑**:CSS Grid 项默认 align-self:stretch 会与 `aspect-ratio:1/1` 冲突把容器拉高成竖椭圆(下方露月牙瓣),网格项必须加 `self-center justify-self-center w-full`,内部铺满元素用 `absolute inset-0` 而非 `h-full`(aspect-ratio 尺寸上百分比高度解析有歧义)
|
||||||
|
- **头像全局同步**:持久 root layout 的 Header 等客户端组件不能只靠 `router.refresh()`;`lib/userEvents.ts` 事件总线(`j13:user-updated` CustomEvent,emitUserUpdate/onUserUpdate)在上传/选用/删除后广播 patch,Header/AvatarPicker 订阅;Avatar 组件 img 带 onError 回退字母头像(附件删除后不裂图),并在 src 变化时重置失败态。**教训:Avatar 默认渲染字母头像,`<Avatar>` 所有调用点必须显式传 `src={xxx.avatar || undefined}`——曾因 Header 用户菜单/PostRow/PinnedPosts/CommentSection/post 详情页 9 处漏传 src 导致全站只显示字母头像(后端 JSON 本就带 user.avatar,纯前端遗漏)**
|
||||||
|
- 注意:CSRF token 是 base64.URLEncoding(含结尾 `=`),从 cookie 读取时不能用 split('=')[1](会截断),要用正则 `([^;]+)`,见 lib/api.ts getCSRFToken
|
||||||
28
.cursor/rules/jiang13-bbs-auth-realtime.mdc
Normal file
28
.cursor/rules/jiang13-bbs-auth-realtime.mdc
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
description: jiang13-bbs 认证 Cookie 方案与公共 WebSocket 实时总线约定
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# 认证与实时总线
|
||||||
|
|
||||||
|
## Cookie 认证(禁止 localStorage token)
|
||||||
|
|
||||||
|
- `j13_token` HttpOnly 15m;`j13_refresh` HttpOnly 7d Path=/;`j13_csrf` 非 HttpOnly
|
||||||
|
- 客户端 `/api/*` + `credentials:'include'`;写请求带 `X-CSRF-Token`
|
||||||
|
- `fetchWithRefresh`:401 自动 refresh 重试;`TokenVersion` 封禁/改密/改角色后使 JWT 失效
|
||||||
|
- SSR:middleware 过期可续 refresh;layout 直出 Header 登录态,避免 F5 闪游客
|
||||||
|
|
||||||
|
## WebSocket(全站一条连接)
|
||||||
|
|
||||||
|
- `GET /api/ws`;dev 直连 `ws://hostname:3001/api/ws`(Next rewrite 不透传 Upgrade)
|
||||||
|
- 事件:`hello` / `pong` / `settings:changed` / `presence:update` / `chat:*` / `notification:new` / `feed:changed` / `moderation:changed`
|
||||||
|
- 消息铁律:**先落库再 WS 广播**;WS 不可靠投递;未读靠 HTTP 对账
|
||||||
|
- 角标优先推送:通知落库统一 `OnNotifyNew`;待审变化推 staff `moderation:changed`;**禁止**再恢复 30s 轮询未读/待审
|
||||||
|
- ThemeSync:推送优先 + visibility/focus 校准,**无定时轮询**
|
||||||
|
- 主题 DOM:禁止对 SSR `#j13-accent-vars` 做 `remove()`;热换肤只用 `#j13-accent-runtime`
|
||||||
|
|
||||||
|
## Git
|
||||||
|
|
||||||
|
- remote:`https://git.iioio.com/freefire/jiang13-bbs.git`,分支 `main`
|
||||||
|
- 自建 Gitea 偶发不稳:`git push` 失败则**原样重试一次**;勿改 remote / 勿反复重试
|
||||||
|
- 勿提交 `node_modules`、`.next`、`backend/cmd/jiang13/data`(含 `.jwt_secret`)、真实 `app.ini`
|
||||||
43
.cursor/rules/jiang13-bbs-core.mdc
Normal file
43
.cursor/rules/jiang13-bbs-core.mdc
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
description: jiang13-bbs 核心铁律(自 Trae 项目记忆导入):技术栈、Next/Go 约束、SEO、权限
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# jiang13-bbs 核心铁律
|
||||||
|
|
||||||
|
详细原文见 [`.cursor/project_memory.md`](../project_memory.md)(自 Trae `project_memory.md` 导入并持续更新)。
|
||||||
|
|
||||||
|
## 记忆初始化指令(必须遵守)
|
||||||
|
|
||||||
|
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'`
|
||||||
|
- 纯 SSR:禁止骨架屏闪动(无 `app/loading.tsx` / Skeleton)
|
||||||
|
|
||||||
|
## Go 1.27
|
||||||
|
|
||||||
|
- 优先泛型方法;`encoding/json/v2`;标准库 uuid;嵌入字段可直接初始化
|
||||||
|
|
||||||
|
## SEO
|
||||||
|
|
||||||
|
- 公开页 SSR/ISR;title/description/OG/JSON-LD 必须在初始 HTML
|
||||||
|
- 帖子详情 `DiscussionForumPosting` JSON-LD;必须有 `sitemap.ts` / `robots.ts`
|
||||||
|
|
||||||
|
## 权限
|
||||||
|
|
||||||
|
- 校验只在 Go;受保护 API 走 RBAC;登录 20/分、注册 10/分、新用户发帖 24h 冷静期
|
||||||
|
- 角色:owner > super_admin > admin > board_admin > user;前端 `lib/roles.ts` 只控制 UI 显隐
|
||||||
29
.cursor/rules/jiang13-bbs-engineering.mdc
Normal file
29
.cursor/rules/jiang13-bbs-engineering.mdc
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
description: jiang13-bbs 工程硬约束与历史踩坑(自 Trae 记忆)
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# 工程约定与踩坑
|
||||||
|
|
||||||
|
## Hard Constraints
|
||||||
|
|
||||||
|
- 主评论计楼层,子评论不计;评论深度上限 10
|
||||||
|
- 详情页三栏宽度与首页统一:`max-w-[1440px]`、`gap-6` / `xl:gap-7`
|
||||||
|
- 禁止操作自己的管理员账号;至少保留一名未封禁管理员;角色白名单
|
||||||
|
- 封禁/降级事务内 `token_version++` + 撤销 refresh;`/api/logout` 在 RequireAuth 外
|
||||||
|
- 确认弹窗用站点化设计,禁止 `window.confirm`;封禁模态挂 root layout
|
||||||
|
|
||||||
|
## 常见坑
|
||||||
|
|
||||||
|
- GORM bool `default:true` 会把 `false` 当零值省略 → 布尔默认用 `false` + `Select` 强制写
|
||||||
|
- pending 帖详情 SSR **必须转发 cookie**,否则作者打开 404/500
|
||||||
|
- GORM 指针关联会建 DB FK:`LastMessage`/`Notification.Room` 必须 `constraint:-`
|
||||||
|
- Avatar 所有调用必须 `src={xxx.avatar || undefined}`
|
||||||
|
- CSRF cookie 是 URLEncoding(含 `=`),解析勿用 `split('=')[1]`
|
||||||
|
- PowerShell 5.1 中文脚本需 UTF-8 BOM;npx/npm 用 `npx.cmd`/`npm.cmd`
|
||||||
|
|
||||||
|
## 用户偏好(自 Trae user_profile)
|
||||||
|
|
||||||
|
- 沟通用中文;重视真 SSR、信息密度、WCAG(正文 ≥4.5:1)
|
||||||
|
- 纯 SSR 站不要骨架屏;少用滚动快照恢复(最后手段)
|
||||||
|
- 配色只改 CSS 令牌(见 skill `theme-recolor-workflow`)
|
||||||
45
.cursor/skills/theme-recolor-workflow/SKILL.md
Normal file
45
.cursor/skills/theme-recolor-workflow/SKILL.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: "theme-recolor-workflow"
|
||||||
|
description: "Recolor/reskin this BBS via design tokens only: clarify direction first, batch-replace CSS variables, verify WCAG AA by script, then CDP dual-theme screenshot regression. Invoke on any 配色/换肤/明暗主题/颜色优化 request."
|
||||||
|
---
|
||||||
|
|
||||||
|
# 姜十三论坛 · 换肤配色工作流(Token-only Reskin)
|
||||||
|
|
||||||
|
本技能把"改配色"固化为可复用流程,防止出现散改 class、补丁式换色、对比度回退、暗色污染等历史踩坑。用户提出配色/换肤/明暗主题不满意时,严格按此执行。
|
||||||
|
|
||||||
|
## 铁律
|
||||||
|
|
||||||
|
1. **只改令牌,不改组件**:唯一入口是 [globals.css](file:///c:/Users/freefire/Documents/jiang13-bbs/frontend/app/globals.css) `:root`(浅色)与 `.dark`(暗色)中的 CSS 变量,以及同文件内的头像色板 `.av-0~.av-7`/`.dark .av-*`、`.btn-primary` 阴影。
|
||||||
|
- 动手前必须先 Grep 确认组件层零硬编码:在 `frontend/components` 与 `frontend/app/**/*.tsx` 搜 `#[0-9a-fA-F]{6}\b|rgba?\(`。若发现硬编码色,先改成 `var(--xxx)` 语义变量,再换肤。
|
||||||
|
- 禁止在组件里写死颜色/内联 `style={{color:'#..'}}`;禁止逐个组件换 Tailwind 色阶。
|
||||||
|
2. **先问方向再动手**:审美反馈("差点意思""不好看")不允许直接换色。用 AskUserQuestion 给 3 个成体系方向(冷调/暖调精修/极简高对比等),每个选项写清浅+暗两组代表色值,用户选定后再改。
|
||||||
|
3. **浅暗成对、语义成组替换**:一次替换必须同时覆盖两套主题的 bg/panel/panel-2/tint、ink/ink-2/ink-3、line/line-2、accent/accent-hi/accent-soft/accent-on、clay/gold/danger/ok(含 *-soft)、nav-active-*、rank-1/2/3、shadow*。暗色宿主统一用 `.dark`(class 挂在 `<html>`)。
|
||||||
|
4. **对比度是硬性验收项**:正文/次要文字、accent 文字、状态色对全部 4 种表面(bg/panel/panel-2/tint)WCAG 比值 ≥ 4.5:1;白色文字压在 accent 按钮上也要 ≥ 4.5:1。用临时 Node 脚本按 WCAG 相对亮度公式核算(脚本放 %TEMP%,用完即删),未达标的色值调深/调亮后重算,不允许"差不多就行"。奖牌 rank 色若用于大号数字装饰可放宽到 ≥3:1,但优先达 4.5。
|
||||||
|
|
||||||
|
## 执行步骤
|
||||||
|
|
||||||
|
1. Read globals.css 令牌区(约前 105 行)与头像区(搜 `头像配色`),盘点全部需替换的变量。
|
||||||
|
2. AskUserQuestion 收敛配色方向(3 选 1)。
|
||||||
|
3. 在 `%TEMP%` 写 `.mjs` 对比度核算脚本(Node 24 直接跑,含 sRGB→线性、L=0.2126R+0.7152G+0.0722B、ratio=(Lmax+.05)/(Lmin+.05)),输出每个候选色对 4 表面的比值;反复调到全 ≥4.5。
|
||||||
|
4. 用 Edit 成组替换 globals.css:标题注释、`:root`、`.dark`、`.btn-primary` 阴影、`.av-*` 两套色板。阴影颜色要与新灰阶同色温(冷调用 `rgba(16,24,40,..)`,暖调用棕)。
|
||||||
|
5. 静态校验:`npx.cmd tsc --noEmit`(cwd=frontend,应 exit 0)。
|
||||||
|
6. CDP 实测回归(headless Edge,流程见下):桌面 ≥1440 视口浅/暗、390px 移动浅/暗、至少 1 个内页(如 /post/12)浅/暗;检查 0 console error、无横滚(scrollWidth=视口宽)、导航高亮/accent 令牌 computed 值正确。截图存 spec evidence 目录并编号续号。
|
||||||
|
7. 清理:删除临时脚本、Stop-Process 掉探针 Edge;向用户汇报新色板 + 对比度结果 + 证据编号。
|
||||||
|
|
||||||
|
## CDP 双主题探针要点(Windows / headless Edge)
|
||||||
|
|
||||||
|
- 启动(run_in_background,端口每次递增避免残留占用):
|
||||||
|
`& "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --headless=new --disable-gpu --remote-debugging-port=922X --user-data-dir="$env:TEMP\edge-cdp-probeN" --no-first-run --no-default-browser-check about:blank`
|
||||||
|
- Node 24 原生 WebSocket/fetch:连 `http://127.0.0.1:922X/json/list` 取 type=page 的 webSocketDebuggerUrl。
|
||||||
|
- **主题 cookie 必须在 Page.navigate 之前写**(`Network.setCookie` name=j13-theme value=light|dark domain=localhost path=/,先 deleteCookies);导航后再兜底 `document.documentElement.classList.toggle('dark', …)`。顺序反了会出现暗色污染。
|
||||||
|
- 换主题要重新 navigate 整页(客户端 toggle class 不刷新 cookie 态)。
|
||||||
|
- 探针脚本放 `%TEMP%`,Node ESM 中动态导入写 `const fs = await import("node:fs")`;JS 字符串内正则反斜杠双转义(`\\s`、`#[0-9a-fA-F]{6}\\b`)。
|
||||||
|
- 左下角红"N"是 Next dev 指示器,不是错误;但如果它显示 "1 Issue",必须用 Runtime.consoleAPICalled/exceptionThrown 监听核实(曾据此抓到过水合不匹配)。
|
||||||
|
- 环境:前端 dev :3000(npm.cmd run dev),后端 :3001(go run,cwd=backend),验证无需生产构建;注意 dev TTFB 上百毫秒属正常,勿当性能缺陷。
|
||||||
|
|
||||||
|
## 常见坑(已踩过)
|
||||||
|
|
||||||
|
- 客户端组件首帧读 `performance`/`window` 等会造成 hydration mismatch;首帧渲染占位("—"),useEffect 后再填真值。
|
||||||
|
- 渐变(如 linear-gradient(var(--panel), var(--panel-2)))走变量会自动适配,无需单独改;先 Grep `gradient|#[0-9a-f]` 再动手。
|
||||||
|
- 不要引入 Tailwind 灰阶(bg-white/text-gray-x)造成局部不跟随主题。
|
||||||
|
- 用户只说改配色时,不动布局、圆角、字体、间距与组件结构。
|
||||||
8
.cursor/user_preferences.md
Normal file
8
.cursor/user_preferences.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
## User Preferences
|
||||||
|
- Communication language: Chinese
|
||||||
|
- Code style: maintain existing code if it has no issues; prioritize code规范 (code standards); avoid implementing scroll position restoration via snapshots (consider it a last-resort solution)
|
||||||
|
- Frontend focus: visual presentation clarity and hierarchical organization of display elements; UI/UX optimization for mobile responsiveness and user-friendliness; mobile editor layout requires only publish bar and toolbar to be sticky at top, with title box, type, board, and tags not sticky
|
||||||
|
- Sticker requirements: prefers high-quality image-based stickers (e.g., Feishu style) over handwritten SVG;颜文字 should be pure text without rendering/styling
|
||||||
|
- SSR validation: uses AITDK SEO browser plugin's SSR check function to verify SSR status; expects clean view-source without unnecessary inline scripts
|
||||||
|
- Project quality: requires genuine, clean SSR implementation with high-quality code
|
||||||
|
- Frontend design: prefers dark Bento futuristic style (style C) but wants to reduce AI-like appearance; requests UI and layout reconstruction based on style C; dislikes current UI/UX design, considers it too ugly; does not want to use skeleton screens in pure SSR websites (F5 hard refresh should not show skeletons); values WCAG contrast standards (text ≥4.5:1, interactive controls ≥3:1) for color schemes; prefers high information density and visually appealing UI design
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -26,12 +26,14 @@ next-debug.log*
|
|||||||
.env.*.local
|
.env.*.local
|
||||||
backend/app.ini
|
backend/app.ini
|
||||||
backend/data/
|
backend/data/
|
||||||
|
backend/cmd/jiang13/data/
|
||||||
*.pem
|
*.pem
|
||||||
*.key
|
*.key
|
||||||
|
|
||||||
# ===== 日志 =====
|
# ===== 日志 =====
|
||||||
*.log
|
*.log
|
||||||
logs/
|
logs/
|
||||||
|
.cursor/debug*.log
|
||||||
|
|
||||||
# ===== 测试与覆盖率 =====
|
# ===== 测试与覆盖率 =====
|
||||||
coverage/
|
coverage/
|
||||||
|
|||||||
Reference in New Issue
Block a user