Skip to content

Commit b020094

Browse files
committed
fix: Chat部分迁移到后端
1 parent d3836ee commit b020094

8 files changed

Lines changed: 105 additions & 77 deletions

File tree

app/api/analytics/route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { prisma } from "@/lib/db";
2+
import { resolveUserId } from "@/lib/server-auth";
23

34
export async function POST(req: Request) {
45
try {
5-
const { eventType, eventData, userId } = await req.json();
6+
const { eventType, eventData } = await req.json();
67

78
if (!eventType) {
89
return Response.json(
@@ -11,11 +12,15 @@ export async function POST(req: Request) {
1112
);
1213
}
1314

15+
// 服务端验证身份,不信任客户端传入的 userId
16+
const userId = await resolveUserId(req);
17+
1418
await prisma.analyticsEvent.create({
1519
data: {
1620
eventType,
1721
eventData: eventData ?? {},
18-
userId: userId ? parseInt(String(userId)) : null,
22+
// userId 对应 user_accounts.id(BigInt);匿名访问为 null
23+
...(userId != null && { userId }),
1924
},
2025
});
2126

app/api/chat/route.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ interface ChatRequest {
2626
chatId?: string;
2727
}
2828

29+
import { resolveUserId } from "@/lib/server-auth";
30+
2931
export async function POST(req: Request) {
3032
try {
33+
// 先把 body 消费掉,再并行验证用户身份
3134
const {
3235
messages,
3336
system,
@@ -37,6 +40,9 @@ export async function POST(req: Request) {
3740
chatId,
3841
}: ChatRequest = await req.json();
3942

43+
// 并行解析用户身份(不阻塞主流程,失败静默降级为匿名)
44+
const userIdPromise = resolveUserId(req);
45+
4046
// 对指定Provider验证key是否存在
4147
if (requiresApiKey(provider) && (!apiKey || apiKey.trim() === "")) {
4248
return Response.json(
@@ -90,8 +96,6 @@ export async function POST(req: Request) {
9096
// 根据Provider获取 AI 模型实例
9197
const model = getModel(provider, apiKey);
9298

93-
// 确保有 chatId (如果前端没传,就生成一个临时的,虽然这会导致每次请求都是新会话)
94-
// 理想情况是前端应该维护 chatId
9599
const effectiveChatId = chatId || crypto.randomUUID();
96100

97101
// 生成流式响应
@@ -101,11 +105,21 @@ export async function POST(req: Request) {
101105
messages: convertToModelMessages(messages || []),
102106
onFinish: async ({ text }) => {
103107
try {
104-
// 1. 保存/更新会话
108+
// 等待用户身份解析(与流式传输并行运行,此时大概率已完成)
109+
const userId = await userIdPromise;
110+
111+
// 1. 保存/更新会话,绑定用户 ID
112+
// update 也写入 userId:覆盖此前匿名创建的记录(用户登录后继续同一 chatId)
105113
await prisma.chat.upsert({
106114
where: { id: effectiveChatId },
107-
update: { updatedAt: new Date() },
108-
create: { id: effectiveChatId },
115+
update: {
116+
updatedAt: new Date(),
117+
...(userId != null && { userId }),
118+
},
119+
create: {
120+
id: effectiveChatId,
121+
...(userId != null && { userId }),
122+
},
109123
});
110124

111125
// 2. 保存用户消息 (取最后一条)

app/components/DocsAssistant.tsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,21 @@ function DocsAssistantInner({ pageContext }: DocsAssistantProps) {
5858
? geminiApiKey
5959
: "";
6060

61-
// 生成唯一的会话 ID
62-
const [chatId] = useState(
63-
() => `chat-${Date.now()}-${Math.random().toString(36).slice(2)}`,
64-
);
61+
// 按 slug 从 localStorage 读取或生成持久化会话 ID
62+
// 同一文档页关闭后再打开依然复用同一 chatId,保持会话连续性
63+
const [chatId] = useState<string>(() => {
64+
// SSR 阶段无法访问 localStorage,生成占位 ID(不影响 DOM,不产生 hydration 警告)
65+
if (typeof window === "undefined") {
66+
return `chat-ssr-${Math.random().toString(36).slice(2)}`;
67+
}
68+
const key = `chat_id:${pageContext.slug ?? "__global__"}`;
69+
const stored = localStorage.getItem(key);
70+
if (stored) return stored;
71+
const newId = `chat-${Date.now()}-${Math.random().toString(36).slice(2)}`;
72+
localStorage.setItem(key, newId);
73+
return newId;
74+
});
75+
6576
const chatRuntimeId = useMemo(
6677
() => `${chatId}:${provider}:${hashTransportConfig(apiKey)}`,
6778
[chatId, provider, apiKey],
@@ -77,6 +88,16 @@ function DocsAssistantInner({ pageContext }: DocsAssistantProps) {
7788
apiKey,
7889
chatId,
7990
},
91+
// 在每次请求时动态读取 satoken,避免用户登录前创建 transport 导致 token 为空
92+
fetch: async (url, init) => {
93+
const token =
94+
typeof window !== "undefined"
95+
? localStorage.getItem("satoken")
96+
: null;
97+
const headers = new Headers(init?.headers);
98+
if (token) headers.set("x-satoken", token);
99+
return fetch(url, { ...init, headers });
100+
},
80101
}),
81102
[pageContext, provider, apiKey, chatId],
82103
);
@@ -97,12 +118,22 @@ function DocsAssistantInner({ pageContext }: DocsAssistantProps) {
97118
const fetchedWelcomeRef = useRef(false);
98119

99120
// 埋点上报函数
121+
// x-satoken 由服务端验证身份,不在 body 里传 userId(服务端自己解析)
100122
const logAnalyticsEvent = useCallback(
101123
async (eventType: string, eventData?: Record<string, unknown>) => {
102124
try {
125+
const token =
126+
typeof window !== "undefined"
127+
? localStorage.getItem("satoken")
128+
: null;
129+
const headers: Record<string, string> = {
130+
"Content-Type": "application/json",
131+
};
132+
if (token) headers["x-satoken"] = token;
133+
103134
await fetch("/api/analytics", {
104135
method: "POST",
105-
headers: { "Content-Type": "application/json" },
136+
headers,
106137
body: JSON.stringify({
107138
eventType,
108139
eventData: {

lib/server-auth.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* 服务端身份验证工具函数(仅用于 Next.js API Route / Server Component)
3+
*
4+
* 通过 x-satoken 请求头调用后端 /auth/me 验证身份,
5+
* 返回 user_accounts.id(BigInt),匿名或 token 无效时返回 null。
6+
*
7+
* 使用方:app/api/chat/route.ts、app/api/analytics/route.ts
8+
*/
9+
export async function resolveUserId(req: Request): Promise<bigint | null> {
10+
const token = req.headers.get("x-satoken");
11+
if (!token || !process.env.BACKEND_URL) return null;
12+
try {
13+
const res = await fetch(`${process.env.BACKEND_URL}/auth/me`, {
14+
headers: { satoken: token },
15+
});
16+
if (!res.ok) return null;
17+
const body = await res.json();
18+
const id = body?.data?.id;
19+
return id != null ? BigInt(id) : null;
20+
} catch {
21+
return null;
22+
}
23+
}

lib/use-auth.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export interface UserView {
1111
enabled: boolean;
1212
roles: string[];
1313
permissions: string[];
14+
avatarUrl: string | null; // GitHub 头像 URL
15+
email: string | null; // GitHub 邮箱
16+
githubId: number | null; // GitHub 数字用户 ID
1417
}
1518

1619
type AuthStatus = "loading" | "authenticated" | "unauthenticated";

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
"lucide-react": "^0.544.0",
6464
"motion": "^12.23.16",
6565
"next": "16.1.5",
66-
"next-auth": "5.0.0-beta.30",
6766
"next-intl": "^4.3.9",
6867
"pg": "^8.18.0",
6968
"react": "19.2.3",

pnpm-lock.yaml

Lines changed: 0 additions & 47 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prisma/schema.prisma

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ datasource db {
77
provider = "postgresql"
88
}
99

10+
// ──────────────────────────────────────────────────────────────────────────────
11+
// NextAuth 遗留表(auth.js 已迁移到后端 Sa-Token,这三张表不再写入新数据)
12+
// users.id 是 Int(自增),与 user_accounts.id(BigInt)是两套独立的用户体系
13+
// ──────────────────────────────────────────────────────────────────────────────
1014
model Account {
1115
id Int @id @default(autoincrement())
12-
userId Int
16+
userId Int // 关联 users.id(NextAuth Int 主键,非 user_accounts)
1317
type String @db.VarChar(255)
1418
provider String @db.VarChar(255)
1519
providerAccountId String @db.VarChar(255)
@@ -59,7 +63,7 @@ model docs {
5963

6064
model Session {
6165
id Int @id @default(autoincrement())
62-
userId Int
66+
userId Int // 关联 users.id(NextAuth Int 主键,非 user_accounts)
6367
expires DateTime @db.Timestamptz(6)
6468
sessionToken String @unique @db.VarChar(255)
6569
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -69,15 +73,13 @@ model Session {
6973
}
7074

7175
model User {
72-
id Int @id @default(autoincrement())
73-
name String? @db.VarChar(255)
74-
email String? @unique @db.VarChar(255)
75-
emailVerified DateTime? @db.Timestamptz(6)
76-
image String?
77-
accounts Account[]
78-
sessions Session[]
79-
chats Chat[]
80-
analyticsEvents AnalyticsEvent[]
76+
id Int @id @default(autoincrement())
77+
name String? @db.VarChar(255)
78+
email String? @unique @db.VarChar(255)
79+
emailVerified DateTime? @db.Timestamptz(6)
80+
image String?
81+
accounts Account[]
82+
sessions Session[]
8183
8284
@@map("users")
8385
}
@@ -105,13 +107,12 @@ model doc_paths {
105107

106108
model Chat {
107109
id String @id @default(cuid())
108-
userId Int?
110+
// 对应 user_accounts.id(Sa-Token 用户),与 Prisma users 表解耦
111+
userId BigInt?
109112
createdAt DateTime @default(now())
110113
updatedAt DateTime @updatedAt
111114
messages Message[]
112115
113-
user User? @relation(fields: [userId], references: [id])
114-
115116
@@index([userId])
116117
}
117118

@@ -129,13 +130,12 @@ model Message {
129130

130131
model AnalyticsEvent {
131132
id String @id @default(cuid())
132-
userId Int?
133+
// 对应 user_accounts.id(Sa-Token 用户),与 Prisma users 表解耦
134+
userId BigInt?
133135
eventType String
134136
eventData Json?
135137
createdAt DateTime @default(now())
136138
137-
user User? @relation(fields: [userId], references: [id])
138-
139139
@@index([eventType])
140140
@@index([userId])
141141
}

0 commit comments

Comments
 (0)