Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 4
refactor: 重构ai对话方法#2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
0e9c8d5a58e6325947a144a477bbdcdf1be476a7a704782ff9a28335f8d542665e409a5db491e9d36ee621f3e1691b79de43e0251749c21c1ed1c5cf3d66bf4b87efaFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| root = true | ||
| [*] | ||
| charset = utf-8 | ||
| indent_style = space | ||
| indent_size = 4 | ||
| end_of_line = lf | ||
| trim_trailing_whitespace = true | ||
| insert_final_newline = true | ||
| [*.properties] | ||
| charset = utf-8 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| # 开发参考手册 | ||
| ## API 端点速查 | ||
| | 说明 | 路径 | | ||
| |------|------| | ||
| | 后端监听 | `http://localhost:8080` | | ||
| | 发起 GitHub 登录(浏览器直跳) | `http://localhost:8080/oauth/render/github` | | ||
| | GitHub 回调(经 Next.js rewrite 代理) | `localhost:3000/api/auth/callback/github` → `localhost:8080/api/auth/callback/github` | | ||
| | 获取当前用户 | `GET /auth/me`(需 `satoken` header) | | ||
| | 退出登录 | `POST /auth/logout`(需 `satoken` header) | | ||
| | 健康检查 | `GET /actuator/health` | | ||
| --- | ||
| ## 用户 ID 体系说明 | ||
| 项目中存在**两套独立的用户 ID**,混淆会导致数据关联错误: | ||
| | 表 | 主键类型 | 管理方 | 说明 | | ||
| |----|---------|--------|------| | ||
| | `users` | `Int`(自增) | Prisma / NextAuth(已迁移,不再写入) | 遗留表,历史数据保留 | | ||
| | `user_accounts` | `BigInt`(自增) | Spring Boot / Sa-Token | **当前有效用户体系** | | ||
| - `Chat.userId` 和 `AnalyticsEvent.userId` 均为 `BigInt`,对应 `user_accounts.id` | ||
| - `doc_contributors.github_id` 为 `BigInt`,对应 `user_accounts.github_id`(GitHub 数字用户 ID) | ||
| - 前端 `UserView.id` 使用 TypeScript `number`,安全范围内(≤ 2^53) | ||
| --- | ||
| ## 前端服务端身份验证 | ||
| 前端 Next.js API Route 通过 `lib/server-auth.ts` 中的 `resolveUserId()` 验证用户: | ||
| ``` | ||
| 请求携带 x-satoken header | ||
| → Next.js API Route 调用 resolveUserId(req) | ||
| → 服务端向后端 GET /auth/me 发起请求(BACKEND_URL 环境变量) | ||
| → 返回 user_accounts.id(BigInt)或 null(匿名) | ||
| ``` | ||
| **使用方:** | ||
| - `frontend/app/api/chat/route.ts` — 保存 Chat 记录时关联用户 | ||
| - `frontend/app/api/analytics/route.ts` — 保存 AnalyticsEvent 时关联用户 | ||
| 不要在 `resolveUserId` 以外的地方重新实现这段逻辑。 | ||
| --- | ||
| ## 前后端职责分工现状(2026-03-29) | ||
| ### 已迁移到后端 | ||
| | 功能 | 迁移前 | 迁移后 | | ||
| |------|--------|--------| | ||
| | GitHub OAuth 登录 | NextAuth(前端) | JustAuth + Sa-Token(后端) | | ||
| | 会话管理 | NextAuth Session / Prisma `sessions` | Sa-Token `user_accounts` | | ||
| | 用户数据 | Prisma `users` 表 | `user_accounts` 表 | | ||
| ### 前端 API Route 现状 | ||
| | 路由 | 说明 | 状态 | | ||
| |------|------|------| | ||
| | `api/chat` | AI 对话,优先代理到后端 `/openai/responses/stream`,失败时 fallback 本地推理 | ⚠️ AI Key 仍分散在前后端,待统一 | | ||
| | `api/analytics` | 埋点写 Neon | 暂留前端,功能自洽 | | ||
| | `api/upload` | 上传到 Cloudflare R2 | 暂留前端,功能自洽 | | ||
| | `api/suggestions` | AI 生成建议问题 | 暂留前端 | | ||
| | `api/docs-tree` | Fumadocs 文档导航树 | 不迁移,Fumadocs 专属 | | ||
| | `api/indexnow` | SEO ping | 不迁移,构建侧逻辑 | | ||
| ### TODO:Chat AI Key 统一 | ||
| **现状:** 前端 `/api/chat` 已优先尝试代理到后端 `/openai/responses/stream`(5s 超时),失败时 fallback 到本地 Vercel AI SDK 推理。代理路径已打通,但 fallback 仍依赖前端自己的 AI Key 和模型配置。 | ||
| **剩余工作:** 确认后端 `/openai/responses/stream` 稳定后,删除前端 fallback 逻辑,移除前端侧 AI Key 配置,AI 推理完全由后端负责。 | ||
| **优先级:** 低,待后端 AI 接口稳定后处理。 | ||
| --- | ||
| ## Sa-Token 会话流程 | ||
| ``` | ||
| 用户点击登录 | ||
| → 前端跳转 /oauth/render/github(后端直接重定向 GitHub) | ||
| → GitHub 回调 /api/auth/callback/github(经 Next.js rewrite 转发给后端) | ||
| → 后端 JustAuth 解析 AuthUser,查找或创建 user_accounts 记录 | ||
| → StpUtil.login(userId) 建立 Sa-Token 会话 | ||
| → 后端重定向到前端首页,URL 携带 ?token=xxx | ||
| → 前端 AuthProvider 读取 token 存入 localStorage,清除 URL 参数 | ||
| → 后续请求通过 x-satoken header 或 satoken header 传递 token | ||
| ``` | ||
Comment on lines
+83
to
+92
CopilotAI | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.involutionhell.backend.common.config; | ||
| import cn.dev33.satoken.interceptor.SaInterceptor; | ||
| import cn.dev33.satoken.router.SaRouter; | ||
| import cn.dev33.satoken.stp.StpUtil; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.web.servlet.config.annotation.InterceptorRegistry; | ||
| import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
| @Configuration | ||
| public class SaTokenConfigure implements WebMvcConfigurer { | ||
| // 注册 SaToken 拦截器 | ||
| @Override | ||
| public void addInterceptors(InterceptorRegistry registry) { | ||
| // 注册 SaToken 拦截器,定义详细认证规则 | ||
| registry.addInterceptor(new SaInterceptor(handler -> { | ||
| // 拦截规则配置 | ||
| SaRouter | ||
| .match("/**") // 拦截所有路由 | ||
| .notMatch("/auth/login") // 账号密码登录 | ||
| .notMatch("/auth/register") // 注册 | ||
| .notMatch("/oauth/render/github") // GitHub OAuth 授权发起 | ||
| .notMatch("/api/auth/callback/github") // GitHub OAuth 回调(路径与 OAuth App 注册保持一致) | ||
| .check(r -> StpUtil.checkLogin()); // 未登录抛出 NotLoginException | ||
Comment on lines
+19
to
+25
CopilotAI | ||
| })).addPathPatterns("/**"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package com.involutionhell.backend.common.config; | ||
| import cn.dev33.satoken.stp.StpInterface; | ||
| import com.involutionhell.backend.usercenter.repository.UserAccountRepository; | ||
| import java.util.List; | ||
| import org.springframework.stereotype.Component; | ||
| /** | ||
| * Sa-Token 权限与角色加载实现。 | ||
| * | ||
| * 项目原来缺少 StpInterface 实现,Sa-Token 找不到实现 Bean 时会回退到默认的空列表, | ||
| * 导致所有 @SaCheckPermission / @SaCheckRole 注解永远校验失败(403), | ||
| * 不管数据库里给用户配了什么权限都没用。这是个生产 Bug,加上这个类才算把权限体系真正接通。 | ||
| * | ||
| * 关于 loginId 类型:Sa-Token 内部把登录 ID 序列化成 String 存储, | ||
| * 即使调用 StpUtil.login(Long) 传入的是 Long,回调这里时运行时类型也是 String, | ||
| * 所以不能直接强转,要先 toString() 再 Long.valueOf()。 | ||
| */ | ||
| @Component | ||
| public class SaTokenPermissionImpl implements StpInterface { | ||
| private final UserAccountRepository userAccountRepository; | ||
| public SaTokenPermissionImpl(UserAccountRepository userAccountRepository) { | ||
| this.userAccountRepository = userAccountRepository; | ||
| } | ||
| /** | ||
| * 返回用户拥有的权限码列表,Sa-Token 执行 @SaCheckPermission 时会调用此方法。 | ||
| * | ||
| * @param loginId 登录 ID,运行时实际类型是 String,不是 Long | ||
| * @param loginType 登录类型,单端场景下为 "login",此处忽略 | ||
| */ | ||
| @Override | ||
| public List<String> getPermissionList(Object loginId, String loginType) { | ||
| // Sa-Token 回传的 loginId 是 String,必须先 toString() 再转 Long | ||
| return userAccountRepository.findById(Long.valueOf(loginId.toString())) | ||
| .map(account -> List.copyOf(account.permissions())) | ||
| .orElse(List.of()); | ||
| } | ||
| /** | ||
| * 返回用户拥有的角色列表,供 @SaCheckRole 使用,逻辑和 getPermissionList 对称。 | ||
| */ | ||
| @Override | ||
| public List<String> getRoleList(Object loginId, String loginType) { | ||
| return userAccountRepository.findById(Long.valueOf(loginId.toString())) | ||
| .map(account -> List.copyOf(account.roles())) | ||
| .orElse(List.of()); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
CopilotAIApr 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
文档这里描述请求携带 x-satoken header,但当前后端配置的 token-name 是 satoken,且测试也使用 satoken header。若确实只支持 satoken,应统一文档表述;若需要兼容 x-satoken,请补充后端读取该 header 的配置/适配层。