Skip to content

refactor: 重构ai对话方法 - #2

Merged
longsizhuo merged 19 commits into
mainfrom
suxiaoyu
Apr 11, 2026
Merged

refactor: 重构ai对话方法#2
longsizhuo merged 19 commits into
mainfrom
suxiaoyu

Conversation

@TinyAlmond

Copy link
Copy Markdown
Collaborator

refactor: 重构ai对话方法

@TinyAlmondTinyAlmond changed the title Suxiaoyurefactor: 重构ai对话方法Apr 7, 2026
@longsizhuo
longsizhuo requested review from Copilot and longsizhuo and removed request for CopilotApril 9, 2026 19:30

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

本 PR 主要围绕“AI 对话流式输出协议重构 + 认证体系迁移”展开:将 OpenAI 流式接口从 Spring SseEmitter 改为输出流直写以适配 Vercel AI SDK 的 stream-data 格式,同时将认证从 Spring Security/JWT 迁移到 JustAuth + Sa-Token,并同步调整路由前缀与用户表结构以支持 GitHub 资料字段。

Changes:

  • OpenAI 流式对话:改为 StreamingResponseBody + 文本流协议转译(0:/e:),并调整网关请求到 /chat/completionsmessages 入参结构
  • 认证/鉴权:引入 Sa-Token + JustAuth GitHub OAuth,新增 OAuth 回调与 Sa-Token 拦截器,替换原 Spring Security 相关逻辑
  • 基础设施与数据:移除 servlet context-path、更新健康检查路径、扩展 user_accounts 表字段(avatar/email/github_id)及相关 DTO/Repository

Reviewed changes

Copilot reviewed 34 out of 34 changed files in this pull request and generated 9 comments.

Show a summary per file
FileDescription
src/test/java/com/involutionhell/backend/usercenter/model/UserAccountTests.java适配 UserAccount 新增字段的构造参数
src/test/java/com/involutionhell/backend/usercenter/controller/AuthControllerIntegrationTests.java调整认证接口路径到 /auth/*
src/test/java/com/involutionhell/backend/support/AbstractWebIntegrationTest.java登录辅助方法改用 /auth/login
src/test/java/com/involutionhell/backend/openai/service/OpenAiStreamServiceTests.javaOutputStream 测试新的 stream-data 输出格式
src/test/java/com/involutionhell/backend/openai/service/HttpOpenAiStreamGatewayTests.java适配 /chat/completionsmessages 请求体
src/test/java/com/involutionhell/backend/openai/controller/OpenAiStreamControllerIntegrationTests.java更新 OpenAI 流式接口路径到 /openai/responses/stream
src/test/java/com/involutionhell/backend/common/error/GlobalExceptionHandlerTests.java从 Spring Security 异常切换到 Sa-Token 异常断言
src/main/resources/schema.sqluser_accounts 新增 avatar/email/github_id 字段
src/main/resources/application.properties移除 context-path,调整 SQL init 默认值,引入 JustAuth 与 Sa-Token 配置
src/main/java/com/involutionhell/backend/usercenter/service/UserCenterService.java使用 Sa-Token 获取当前用户;新增创建用户与刷新 GitHub 资料方法
src/main/java/com/involutionhell/backend/usercenter/service/AuthService.java使用 Sa-Token 登录/退出;新增 GitHub OAuth 登录并自动注册/刷新资料
src/main/java/com/involutionhell/backend/usercenter/repository/UserAccountRepository.java新增 insertupdateProfile 仓库接口
src/main/java/com/involutionhell/backend/usercenter/repository/JdbcUserAccountRepository.java映射/写入新增资料列;实现插入与资料更新
src/main/java/com/involutionhell/backend/usercenter/model/UserAccount.javaUserAccount 记录新增头像/邮箱/GitHubId 字段
src/main/java/com/involutionhell/backend/usercenter/HealthTestController.java修正 Controller 基路径声明方式(@RequestMapping
src/main/java/com/involutionhell/backend/usercenter/dto/UserView.javaUserView 增加头像/邮箱/GitHubId 字段
src/main/java/com/involutionhell/backend/usercenter/controller/UserCenterController.java路由重构为 /users/* 并改为 Sa-Token 权限注解
src/main/java/com/involutionhell/backend/usercenter/controller/OAuthController.java新增:JustAuth GitHub OAuth 发起与回调处理
src/main/java/com/involutionhell/backend/usercenter/controller/AuthController.java路由重构为 /auth/* 并改为 Sa-Token 登录注解
src/main/java/com/involutionhell/backend/usercenter/config/SecurityConfig.java旧 Spring Security 配置整体注释(迁移占位)
src/main/java/com/involutionhell/backend/openai/service/OpenAiStreamService.java核心重构:从 SSE 事件转发改为输出流协议转译
src/main/java/com/involutionhell/backend/openai/service/HttpOpenAiStreamGateway.java请求体改为 chat.completions + messages;拼接 endpoint;增加未配置告警
src/main/java/com/involutionhell/backend/openai/dto/OpenAiStreamRequest.javaDTO 从 message 改为 messages,匹配 Vercel AI SDK
src/main/java/com/involutionhell/backend/openai/controller/OpenAiStreamController.java控制器改用 StreamingResponseBody 返回纯文本流
src/main/java/com/involutionhell/backend/common/error/GlobalExceptionHandler.java增加 Sa-Token 异常处理分支并调整未预期异常处理
src/main/java/com/involutionhell/backend/common/config/SaTokenConfigure.java新增:Sa-Token WebMVC 拦截器统一登录拦截规则
README.md更新默认入口与示例请求路径(去掉 /api/v1 前缀)
pom.xml引入 JustAuth + Sa-Token,注释 Spring Security 依赖
docs/dev1.md新增开发手册,记录端点与会话流程
docker-compose.yml更新健康检查路径到 /actuator/health
Caddyfile更新 upstream health check URI 到 /actuator/health
.github/workflows/deploy.yml更新部署后健康检查路径到 /actuator/health
.env.example更新 SQL init 默认建议与健康检查 show-details 建议值
.editorconfig新增基础 EditorConfig 规范
Comments suppressed due to low confidence (3)

src/test/java/com/involutionhell/backend/openai/controller/OpenAiStreamControllerIntegrationTests.java:37

  • 该集成测试仍在发送旧的请求体字段 "message",并断言 TEXT_EVENT_STREAM + OpenAI Responses API 的事件名;但当前控制器 DTO 已改为 messages 数组且返回 TEXT_PLAIN 的 Vercel stream-data 格式。这会导致测试无法反映真实行为并直接失败。建议更新请求 JSON、stub 网关返回的示例流,以及断言逻辑以匹配新协议(例如包含 0:"..."/e:"..." 片段)。
 void streamReturnsSseEventsForAuthenticatedUser() throws Exception {
String token = loginAsAdmin();
MvcResult mvcResult = mockMvc.perform(post("/openai/responses/stream")
.header("satoken", token)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"message": "你好"
}
"""))

src/test/java/com/involutionhell/backend/usercenter/controller/AuthControllerIntegrationTests.java:79

  • 匿名请求 /auth/me 的断言仍期望旧的统一文案“未登录或登录状态已失效”,但 GlobalExceptionHandler 现在对 NotLoginException.NOT_TOKEN 返回更具体的“未提供 Token”。在 SaTokenConfigure 拦截器生效时,这里实际会走 NotLoginException 分支。建议同步更新测试期望(或如果仍想保留旧文案,则需要调整异常处理映射)。
    src/test/java/com/involutionhell/backend/usercenter/controller/AuthControllerIntegrationTests.java:101
  • 匿名请求 /auth/logout 的断言仍期望旧的统一文案“未登录或登录状态已失效”,但当前全局异常处理对未提供 token 会返回“未提供 Token”。建议与新的 Sa-Token 异常文案保持一致,避免测试与实际行为不匹配。

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +19 to +25
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

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sa-Token 拦截器目前拦截了所有路由但未放行 /actuator/(以及可能的 /health_check/)。这会导致容器/CI 的健康检查在未携带 token 时直接返回 401,从而判定服务不可用。建议在路由规则中显式 notMatch("/actuator/**")(必要时也放行健康检查/静态资源等)。

Copilot uses AI. Check for mistakes.
Comment on lines 3 to 15
CREATE TABLE IF NOT EXISTS user_accounts (
id BIGSERIAL PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(255),
enabled BOOLEAN NOT NULL DEFAULT TRUE,
roles TEXT NOT NULL DEFAULT '',
permissions TEXT NOT NULL DEFAULT ''
permissions TEXT NOT NULL DEFAULT '',
avatar_url VARCHAR(500),
email VARCHAR(255),
-- github_id 存储 GitHub 数字用户 ID,与 doc_contributors.github_id 对应
github_id BIGINT UNIQUE
);

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里通过 CREATE TABLE IF NOT EXISTS 增加 avatar_url/email/github_id 并不会对已存在的 user_accounts 表生效,但 JdbcUserAccountRepository 使用 SELECT * 并读取这些新列;线上已有表未执行迁移会在查询时报“列不存在”。建议补充显式 ALTER TABLE 迁移脚本(或引入 Flyway/Liquibase),并确保部署流程会执行该迁移。

Copilot uses AI. Check for mistakes.
Comment on lines +41 to 47
* 获取指定用户的详细信息
*/
@PreAuthorize("hasAuthority('user:center:read')")
@GetMapping("/users/{userId}")
@SaCheckPermission("user:profile:read")
@GetMapping("/{userId}")
public ApiResponse<UserView> getUser(@PathVariable Long userId) {
return ApiResponse.ok(userCenterService.getUser(userId));
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getUser() 现在仅要求 user:profile:read 权限,但该接口按 userId 获取任意用户详情(不一定是本人),相较原先的 user:center:read 会放宽访问范围,可能造成越权读取其他用户信息。建议改回更严格的管理权限(例如 user:center:read),或改为仅允许查询本人并在服务层校验 userId == 当前登录用户。

Copilot uses AI. Check for mistakes.
Comment on lines +75 to 93
try {
JsonNode jsonNode = objectMapper.readTree(payload);

// 深入臃肿的大树内部:直接去 choices 第一组的 delta 里面寻找关键节点唯一的 content
JsonNode deltaNode = jsonNode.path("choices").path(0).path("delta").path("content");

// 并不是所有的 JSON 都有字(第一包可能只包含角色分配)
if (!deltaNode.isMissingNode() && deltaNode.isTextual()) {
String textChunk = deltaNode.asText();
// ★ 最为关键的协议转译:对原生纯文本加上 Vercel Stream Text 前导符 '0:' 结合 JSON_Escaped_String 和强回车。
String vercelChunk = "0:" + objectMapper.writeValueAsString(textChunk) + "\n";

outputStream.write(vercelChunk.getBytes(StandardCharsets.UTF_8));
// 高频推送缓冲区,使前端能产生实时的视觉卡顿流水效果!
outputStream.flush();
}
} catch (Exception ignored) {
// 出于防守目的,故意默默捕捉并忽略空包异常,而不让流水中断。
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relayEvents() 里对每个 data payload 的 JSON 解析使用 catch (Exception ignored) 直接吞掉所有异常,可能在上游协议变更/返回错误内容时静默产出空响应,排障困难。建议至少记录一次可控日志(debug/warn)或在连续解析失败时向下游发送 e: 错误块并终止流。

Copilot uses AI. Check for mistakes.
Comment on lines 89 to 93
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleUnexpected(Exception exception) {
exception.printStackTrace(); // 建议在开发阶段打印堆栈,生产环境应使用日志框架
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.fail("服务器内部错误"));

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleUnexpected() 中直接 exception.printStackTrace() 会把异常堆栈输出到标准输出,生产环境下不利于日志治理且可能泄露内部信息。建议改用统一的日志框架记录(例如 logger.error("...", exception)),并由日志配置控制不同环境的输出级别。

Copilot uses AI. Check for mistakes.
Comment threaddocs/dev1.md
Comment on lines +33 to +40
前端 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(匿名)
```

CopilotAIApr 9, 2026

Copy link

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 的配置/适配层。

Copilot uses AI. Check for mistakes.
Comment threaddocs/dev1.md
Comment on lines +83 to +92
```
用户点击登录
→ 前端跳转 /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
```

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里写“URL 携带 ?token=xxx”,但 OAuthController 实际重定向使用的是 fragment 形式 /#token=...。建议统一文档与实现,避免前端按 querystring 解析导致拿不到 token。

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +49
@PostConstruct
void warnIfNotConfigured() {
if (!StringUtils.hasText(properties.apiKey())) {
log.warn("[OpenAI] OPENAI_API_KEY 未配置,/openai/responses/stream 调用时将返回 400 错误");
}
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warnIfNotConfigured() 的日志提示“调用时将返回 400 错误”,但当前 OpenAiStreamController 固定返回 200 + StreamingResponseBody,且 OpenAiStreamService 会捕获 validateConfiguration/openStream 的异常并写入 e: 错误块而不抛出异常,因此通常不会触发全局异常处理返回 400。建议统一行为:要么在 controller 层先 validate 并抛出以返回 400,要么把日志文案改为“将返回 e: 错误块”。

Copilot uses AI. Check for mistakes.
Comment on lines 116 to +121
HttpRequest buildHttpRequest(String requestBody) {
return HttpRequest.newBuilder(URI.create(properties.apiUrl()))
String apiUrl = properties.apiUrl();
// 自动容错:如果环境变量里仅配置了根基地址,自动帮忙拼接聊天补全入口。
if (!apiUrl.endsWith("/chat/completions")) {
apiUrl = apiUrl.replaceAll("/+$", "") + "/chat/completions";
}

CopilotAIApr 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildHttpRequest() 目前对任何不以 /chat/completions 结尾的 apiUrl 都直接追加 "/chat/completions"。如果现有配置仍是旧值(例如测试配置里使用过的 https://api.openai.com/v1/responses),会被拼成 https://.../v1/responses/chat/completions,导致请求 404。建议明确约束 apiUrl 为 base(如 .../v1)并在校验时拒绝包含旧路径,或在这里对以 /responses 结尾的情况做兼容替换。

Copilot uses AI. Check for mistakes.
claudeand others added 4 commits April 10, 2026 20:39
New test suites:
- JdbcUserAccountRepositoryTests: 13 tests covering all CRUD operations,
null handling, and error paths with H2 in-memory database
- AuthServiceTests: 13 unit tests for login/loginByGithub/logout/currentUser
using Mockito with mockStatic for Sa-Token isolation
- OAuthControllerIntegrationTests: 4 tests covering GitHub OAuth redirect
URL validation and callback failure paths
Infrastructure fixes:
- AbstractWebIntegrationTest: add @SpringBootTest(properties) override to
prevent SPRING_DATASOURCE_URL env var from overriding H2 test config
- BackendApplicationTests: same H2 override fix for context load test
- test-schema.sql: add missing avatar_url, email, github_id columns that
JdbcUserAccountRepository.ROW_MAPPER reads
Production bug fixes revealed by tests:
- SaTokenPermissionImpl: implement StpInterface so @SaCheckPermission
annotations can resolve permissions from the database (was always 403)
- GlobalExceptionHandler: use e.getPermission() instead of e.getCode()
in handleNotPermissionException (getCode() returned -1, not the permission)
Pre-existing test fixes:
- AuthControllerIntegrationTests: update expected Sa-Token error messages
from old generic message to current specific messages ("未提供 Token")
- UserCenterControllerIntegrationTests: fix stale URL paths (/users instead
of /api/user-center/users) and update expected permission error messages
- OpenAiStreamControllerIntegrationTests: update for renamed DTO field
(message -> messages), fix asyncDispatch pattern for StreamingResponseBody,
and align stub gateway with OpenAI API format expected by relayEvents()
Result: 78/78 tests pass (was 40/78 with 36 errors + 2 failures baseline)
https://claude.ai/code/session_016Z9qEQdrSXSTAhCp1YMgnk
Root causes and fixes:
1. SaTokenPermissionImpl (NEW)
- Without StpInterface, Sa-Token returns empty permissions for all users,
causing every @SaCheckPermission check to unconditionally fail with 403.
- loginId must be parsed via toString() first: Sa-Token serializes it as
String internally even when StpUtil.login(Long) was called.
2. GlobalExceptionHandler
- NotPermissionException.getCode() returns the integer scene code (-1),
not the permission string. Fixed to getPermission() which returns the
actual missing permission name (e.g. "user:center:read").
3. AbstractWebIntegrationTest + BackendApplicationTests
- SPRING_DATASOURCE_URL env var (pointing to Neon PostgreSQL) has higher
Spring priority than application-test.properties, causing H2 context
load failure. Fixed via @SpringBootTest(properties) which overrides all
env vars.
- JustAuth UrlValidator rejects localhost redirect URIs; overridden with
a syntactically valid placeholder URL.
4. AuthControllerIntegrationTests
- Sa-Token NOT_TOKEN scenario now maps to "未提供 Token", not the old
generic "未登录或登录状态已失效" message.
5. UserCenterControllerIntegrationTests
- All URLs updated: /api/user-center/* paths were removed; current routes
are /auth/me, /users, /users/{id}, /users/{id}/authorization.
- Permission error message updated to match GlobalExceptionHandler output:
"拒绝访问: 缺少权限 [<permission>]".
6. OpenAiStreamControllerIntegrationTests
- DTO field renamed: message (String) -> messages (List), aligning with
Vercel AI SDK payload format.
- Async test uses asyncDispatch two-step pattern (required for
StreamingResponseBody); polling getContentAsString() never sees data.
- Stub returns OpenAI SSE format choices[0].delta.content so relayEvents()
can extract the text and emit Vercel Stream prefix "0:".
All 78 tests now pass.
https://claude.ai/code/session_016Z9qEQdrSXSTAhCp1YMgnk
Removed all <h3>, <h4>, <p>, <ul>, <li>, <ol> tags from Javadoc blocks.
Rewrote everything in plain Chinese as a developer would naturally write it.
https://claude.ai/code/session_016Z9qEQdrSXSTAhCp1YMgnk
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@TinyAlmond@longsizhuo@claude