Skip to content

Commit 4a50279

Browse files
authored
Merge pull request #2 from InvolutionHell/suxiaoyu
refactor: 重构ai对话方法
2 parents f5ab3dd + 4b87efa commit 4a50279

41 files changed

Lines changed: 1663 additions & 531 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.editorconfig‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
indent_style = space
6+
indent_size = 4
7+
end_of_line = lf
8+
trim_trailing_whitespace = true
9+
insert_final_newline = true
10+
11+
[*.properties]
12+
charset = utf-8

‎.env.example‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ PGSSLMODE=require
88

99
# Spring Boot JDBC 连接(由上面的 PG 变量转换而来)
1010
SPRING_DATASOURCE_URL=jdbc:postgresql://ep-xxxx.ap-southeast-2.aws.neon.tech/neondb?sslmode=require
11-
SPRING_SQL_INIT_MODE=always
11+
# 首次部署时设为 always 以初始化 schema.sql,之后改为 never
12+
SPRING_SQL_INIT_MODE=never
1213

1314
# --- 本地开发用 Docker PostgreSQL(无 Neon 账号的开发者使用)---
1415
POSTGRES_DB=involution_hell
@@ -28,11 +29,15 @@ OPENAI_MODEL=gpt-4.1
2829
# --- 应用基本设置 ---
2930
SPRING_APPLICATION_NAME=backend
3031
SERVER_PORT=8080
32+
# 前端域名,用于 GitHub OAuth 回调重定向和 Sa-Token 会话重定向
33+
# 生产环境必须设置为实际域名,否则登录后会重定向到 localhost
34+
AUTH_URL=https://involutionhell.com
3135

3236
# --- Actuator ---
3337
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE=health,info,metrics
3438
MANAGEMENT_ENDPOINT_HEALTH_PROBES_ENABLED=true
35-
MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS=always
39+
# when-authorized 生产推荐;开发时可改为 always
40+
MANAGEMENT_ENDPOINT_HEALTH_SHOW_DETAILS=when-authorized
3641

3742
# --- 网关(Caddy)---
3843
CADDY_HTTP_PORT=80

‎.github/workflows/deploy.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
# 等待服务启动,最多 60 秒
4848
echo "等待服务健康检查..."
4949
for i in $(seq 1 12); do
50-
if docker exec involution-hell-backend curl -fsS "http://127.0.0.1:8080/api/v1/actuator/health" | grep -q '"status":"UP"'; then
50+
if docker exec involution-hell-backend curl -fsS "http://127.0.0.1:8080/actuator/health" | grep -q '"status":"UP"'; then
5151
echo "✅ 部署成功,服务正常运行"
5252
exit 0
5353
fi

‎Caddyfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
{$CADDY_SITE_ADDRESS::80} {
44
# 将所有请求转发至后端 Spring Boot 服务
55
reverse_proxy {$CADDY_UPSTREAM:backend:8080} {
6-
health_uri /api/v1/actuator/health
6+
health_uri /actuator/health
77
}
88
}

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ docker compose up -d postgres redis
9595
./mvnw spring-boot:run
9696
```
9797

98-
默认接口入口:`http://127.0.0.1:8080/api/v1`
98+
默认接口入口:`http://127.0.0.1:8080`
9999

100100
### 4. 调用示例接口
101101
内置种子账号:
@@ -104,7 +104,7 @@ docker compose up -d postgres redis
104104

105105
登录示例:
106106
```bash
107-
curl -X POST http://127.0.0.1:8080/api/auth/login \
107+
curl -X POST http://127.0.0.1:8080/auth/login \
108108
-H "Content-Type: application/json" \
109109
-d '{"username": "admin", "password": "Admin@123456"}'
110110
```
@@ -136,7 +136,7 @@ push to main
136136
**生产环境健康检查端点:**
137137

138138
```
139-
GET https://api.involutionhell.com/api/v1/actuator/health
139+
GET https://api.involutionhell.com/actuator/health
140140
```
141141

142142
返回 `{"status":"UP"}` 表示服务正常运行。

‎docker-compose.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ services:
2828
expose:
2929
- "${SERVER_PORT:-8080}"
3030
healthcheck:
31-
test: ["CMD-SHELL", "curl -fsS \"http://127.0.0.1:${SERVER_PORT:-8080}/api/v1/actuator/health\" | grep '\"status\":\"UP\"' >/dev/null || exit 1"]
31+
test: ["CMD-SHELL", "curl -fsS \"http://127.0.0.1:${SERVER_PORT:-8080}/actuator/health\" | grep '\"status\":\"UP\"' >/dev/null || exit 1"]
3232
interval: 30s
3333
timeout: 5s
3434
retries: 3

‎docs/dev1.md‎

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# 开发参考手册
2+
3+
## API 端点速查
4+
5+
| 说明 | 路径 |
6+
|------|------|
7+
| 后端监听 | `http://localhost:8080` |
8+
| 发起 GitHub 登录(浏览器直跳) | `http://localhost:8080/oauth/render/github` |
9+
| GitHub 回调(经 Next.js rewrite 代理) | `localhost:3000/api/auth/callback/github` → `localhost:8080/api/auth/callback/github` |
10+
| 获取当前用户 | `GET /auth/me`(需 `satoken` header) |
11+
| 退出登录 | `POST /auth/logout`(需 `satoken` header) |
12+
| 健康检查 | `GET /actuator/health` |
13+
14+
---
15+
16+
## 用户 ID 体系说明
17+
18+
项目中存在**两套独立的用户 ID**,混淆会导致数据关联错误:
19+
20+
| 表 | 主键类型 | 管理方 | 说明 |
21+
|----|---------|--------|------|
22+
| `users` | `Int`(自增) | Prisma / NextAuth(已迁移,不再写入) | 遗留表,历史数据保留 |
23+
| `user_accounts` | `BigInt`(自增) | Spring Boot / Sa-Token | **当前有效用户体系** |
24+
25+
- `Chat.userId` 和 `AnalyticsEvent.userId` 均为 `BigInt`,对应 `user_accounts.id`
26+
- `doc_contributors.github_id` 为 `BigInt`,对应 `user_accounts.github_id`(GitHub 数字用户 ID)
27+
- 前端 `UserView.id` 使用 TypeScript `number`,安全范围内(≤ 2^53)
28+
29+
---
30+
31+
## 前端服务端身份验证
32+
33+
前端 Next.js API Route 通过 `lib/server-auth.ts` 中的 `resolveUserId()` 验证用户:
34+
35+
```
36+
请求携带 x-satoken header
37+
→ Next.js API Route 调用 resolveUserId(req)
38+
→ 服务端向后端 GET /auth/me 发起请求(BACKEND_URL 环境变量)
39+
→ 返回 user_accounts.id(BigInt)或 null(匿名)
40+
```
41+
42+
**使用方:**
43+
- `frontend/app/api/chat/route.ts` — 保存 Chat 记录时关联用户
44+
- `frontend/app/api/analytics/route.ts` — 保存 AnalyticsEvent 时关联用户
45+
46+
不要在 `resolveUserId` 以外的地方重新实现这段逻辑。
47+
48+
---
49+
50+
## 前后端职责分工现状(2026-03-29)
51+
52+
### 已迁移到后端
53+
54+
| 功能 | 迁移前 | 迁移后 |
55+
|------|--------|--------|
56+
| GitHub OAuth 登录 | NextAuth(前端) | JustAuth + Sa-Token(后端) |
57+
| 会话管理 | NextAuth Session / Prisma `sessions` | Sa-Token `user_accounts` |
58+
| 用户数据 | Prisma `users` 表 | `user_accounts` 表 |
59+
60+
### 前端 API Route 现状
61+
62+
| 路由 | 说明 | 状态 |
63+
|------|------|------|
64+
| `api/chat` | AI 对话,优先代理到后端 `/openai/responses/stream`,失败时 fallback 本地推理 | ⚠️ AI Key 仍分散在前后端,待统一 |
65+
| `api/analytics` | 埋点写 Neon | 暂留前端,功能自洽 |
66+
| `api/upload` | 上传到 Cloudflare R2 | 暂留前端,功能自洽 |
67+
| `api/suggestions` | AI 生成建议问题 | 暂留前端 |
68+
| `api/docs-tree` | Fumadocs 文档导航树 | 不迁移,Fumadocs 专属 |
69+
| `api/indexnow` | SEO ping | 不迁移,构建侧逻辑 |
70+
71+
### TODO:Chat AI Key 统一
72+
73+
**现状:** 前端 `/api/chat` 已优先尝试代理到后端 `/openai/responses/stream`(5s 超时),失败时 fallback 到本地 Vercel AI SDK 推理。代理路径已打通,但 fallback 仍依赖前端自己的 AI Key 和模型配置。
74+
75+
**剩余工作:** 确认后端 `/openai/responses/stream` 稳定后,删除前端 fallback 逻辑,移除前端侧 AI Key 配置,AI 推理完全由后端负责。
76+
77+
**优先级:** 低,待后端 AI 接口稳定后处理。
78+
79+
---
80+
81+
## Sa-Token 会话流程
82+
83+
```
84+
用户点击登录
85+
→ 前端跳转 /oauth/render/github(后端直接重定向 GitHub)
86+
→ GitHub 回调 /api/auth/callback/github(经 Next.js rewrite 转发给后端)
87+
→ 后端 JustAuth 解析 AuthUser,查找或创建 user_accounts 记录
88+
→ StpUtil.login(userId) 建立 Sa-Token 会话
89+
→ 后端重定向到前端首页,URL 携带 ?token=xxx
90+
→ 前端 AuthProvider 读取 token 存入 localStorage,清除 URL 参数
91+
→ 后续请求通过 x-satoken header 或 satoken header 传递 token
92+
```

‎pom.xml‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@
6161
<artifactId>spring-boot-starter-jdbc</artifactId>
6262
</dependency>
6363

64-
<!-- spring security & oauth2 -->
64+
<!-- spring security & oauth2 (Temporarily Commented Out for JustAuth Migration) -->
65+
<!--
6566
<dependency>
6667
<groupId>org.springframework.boot</groupId>
6768
<artifactId>spring-boot-starter-security</artifactId>
@@ -74,6 +75,25 @@
7475
<groupId>org.springframework.boot</groupId>
7576
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
7677
</dependency>
78+
-->
79+
80+
<!-- JustAuth Core & Http Client -->
81+
<dependency>
82+
<groupId>me.zhyd.oauth</groupId>
83+
<artifactId>JustAuth</artifactId>
84+
<version>1.16.6</version>
85+
</dependency>
86+
<dependency>
87+
<groupId>cn.hutool</groupId>
88+
<artifactId>hutool-http</artifactId>
89+
<version>5.8.25</version>
90+
</dependency>
91+
<!-- 添加 JustAuth 必须的 json 解析器 (Hutool 的依赖) -->
92+
<dependency>
93+
<groupId>cn.hutool</groupId>
94+
<artifactId>hutool-json</artifactId>
95+
<version>5.8.25</version>
96+
</dependency>
7797

7898
<!-- postgresql -->
7999
<dependency>
@@ -82,6 +102,13 @@
82102
<scope>runtime</scope>
83103
</dependency>
84104

105+
<!-- Sa-Token 权限认证 -->
106+
<dependency>
107+
<groupId>cn.dev33</groupId>
108+
<artifactId>sa-token-spring-boot3-starter</artifactId>
109+
<version>1.39.0</version>
110+
</dependency>
111+
85112
<!-- &lt;!&ndash; redis &ndash;&gt;-->
86113
<!-- <dependency>-->
87114
<!-- <groupId>org.springframework.boot</groupId>-->
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.involutionhell.backend.common.config;
2+
3+
import cn.dev33.satoken.interceptor.SaInterceptor;
4+
import cn.dev33.satoken.router.SaRouter;
5+
import cn.dev33.satoken.stp.StpUtil;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
8+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
9+
10+
@Configuration
11+
public class SaTokenConfigure implements WebMvcConfigurer {
12+
13+
// 注册 SaToken 拦截器
14+
@Override
15+
public void addInterceptors(InterceptorRegistry registry) {
16+
// 注册 SaToken 拦截器,定义详细认证规则
17+
registry.addInterceptor(new SaInterceptor(handler -> {
18+
// 拦截规则配置
19+
SaRouter
20+
.match("/**") // 拦截所有路由
21+
.notMatch("/auth/login") // 账号密码登录
22+
.notMatch("/auth/register") // 注册
23+
.notMatch("/oauth/render/github") // GitHub OAuth 授权发起
24+
.notMatch("/api/auth/callback/github") // GitHub OAuth 回调(路径与 OAuth App 注册保持一致)
25+
.check(r -> StpUtil.checkLogin()); // 未登录抛出 NotLoginException
26+
})).addPathPatterns("/**");
27+
}
28+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.involutionhell.backend.common.config;
2+
3+
import cn.dev33.satoken.stp.StpInterface;
4+
import com.involutionhell.backend.usercenter.repository.UserAccountRepository;
5+
import java.util.List;
6+
import org.springframework.stereotype.Component;
7+
8+
/**
9+
* Sa-Token 权限与角色加载实现。
10+
*
11+
* 项目原来缺少 StpInterface 实现,Sa-Token 找不到实现 Bean 时会回退到默认的空列表,
12+
* 导致所有 @SaCheckPermission / @SaCheckRole 注解永远校验失败(403),
13+
* 不管数据库里给用户配了什么权限都没用。这是个生产 Bug,加上这个类才算把权限体系真正接通。
14+
*
15+
* 关于 loginId 类型:Sa-Token 内部把登录 ID 序列化成 String 存储,
16+
* 即使调用 StpUtil.login(Long) 传入的是 Long,回调这里时运行时类型也是 String,
17+
* 所以不能直接强转,要先 toString() 再 Long.valueOf()。
18+
*/
19+
@Component
20+
public class SaTokenPermissionImpl implements StpInterface {
21+
22+
private final UserAccountRepository userAccountRepository;
23+
24+
public SaTokenPermissionImpl(UserAccountRepository userAccountRepository) {
25+
this.userAccountRepository = userAccountRepository;
26+
}
27+
28+
/**
29+
* 返回用户拥有的权限码列表,Sa-Token 执行 @SaCheckPermission 时会调用此方法。
30+
*
31+
* @param loginId 登录 ID,运行时实际类型是 String,不是 Long
32+
* @param loginType 登录类型,单端场景下为 "login",此处忽略
33+
*/
34+
@Override
35+
public List<String> getPermissionList(Object loginId, String loginType) {
36+
// Sa-Token 回传的 loginId 是 String,必须先 toString() 再转 Long
37+
return userAccountRepository.findById(Long.valueOf(loginId.toString()))
38+
.map(account -> List.copyOf(account.permissions()))
39+
.orElse(List.of());
40+
}
41+
42+
/**
43+
* 返回用户拥有的角色列表,供 @SaCheckRole 使用,逻辑和 getPermissionList 对称。
44+
*/
45+
@Override
46+
public List<String> getRoleList(Object loginId, String loginType) {
47+
return userAccountRepository.findById(Long.valueOf(loginId.toString()))
48+
.map(account -> List.copyOf(account.roles()))
49+
.orElse(List.of());
50+
}
51+
}

0 commit comments

Comments
 (0)