docs: 补充用户模块 REST API 接口契约
- PLAN_USER_MODULE.md 新增「前端 API 接口参考」章节 - 03-接口文档.md 同步认证接口、对话接口、WebSocket 认证变更 - 新增错误码 USERNAME_TAKEN / INVALID_CREDENTIALS / INVALID_TOKEN / INVALID_INPUT - 数据模型补充 User / ConversationSummary / StoredMessage 及对应 TypeScript 类型 - 配置结构体补充 AuthConfig(JWTSecret / AccessTTL / RefreshTTL)
This commit is contained in:
@@ -713,6 +713,588 @@ func main() {
|
||||
|
||||
---
|
||||
|
||||
## 前端 API 接口参考
|
||||
|
||||
本章节为前端开发者提供完整的 REST API 契约。所有接口以 JSON 通信,基地址与 WebSocket 同源(开发环境 `http://localhost:8080`,生产环境通过 Nginx 反代)。
|
||||
|
||||
### 通用约定
|
||||
|
||||
#### 认证方式
|
||||
|
||||
需要认证的接口在请求头携带 JWT access token:
|
||||
|
||||
```
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
未认证或 token 过期时返回 `401 Unauthorized`。
|
||||
|
||||
#### 错误响应格式
|
||||
|
||||
所有错误响应统一结构:
|
||||
|
||||
```typescript
|
||||
interface ApiError {
|
||||
code: string; // 机器可读错误码
|
||||
message: string; // 人类可读描述
|
||||
}
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "USERNAME_TAKEN",
|
||||
"message": "username already taken"
|
||||
}
|
||||
```
|
||||
|
||||
#### 新增错误码
|
||||
|
||||
| 错误码 | HTTP 状态码 | 含义 |
|
||||
|--------|-----------|------|
|
||||
| `USERNAME_TAKEN` | 409 | 用户名已被注册 |
|
||||
| `INVALID_CREDENTIALS` | 401 | 用户名或密码错误 |
|
||||
| `INVALID_TOKEN` | 401 | JWT 无效或已过期 |
|
||||
| `INVALID_INPUT` | 400 | 请求参数校验失败 |
|
||||
| `SESSION_NOT_FOUND` | 404 | 对话不存在或无权访问 |
|
||||
|
||||
#### 输入校验规则
|
||||
|
||||
| 字段 | 规则 |
|
||||
|------|------|
|
||||
| `username` | 3-64 字符,仅允许字母、数字、下划线 |
|
||||
| `password` | 8-72 字符 |
|
||||
|
||||
---
|
||||
|
||||
### 一、认证接口(`/api/auth`)
|
||||
|
||||
#### 1.1 注册
|
||||
|
||||
```
|
||||
POST /api/auth/register
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```typescript
|
||||
interface RegisterRequest {
|
||||
username: string; // 3-64 字符
|
||||
password: string; // 8-72 字符
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应** `201 Created`:
|
||||
|
||||
```typescript
|
||||
interface AuthResponse {
|
||||
user: {
|
||||
id: string; // UUID
|
||||
username: string;
|
||||
created_at: string; // ISO 8601
|
||||
};
|
||||
access_token: string; // JWT,15 分钟有效
|
||||
refresh_token: string; // JWT,7 天有效
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"username": "alice",
|
||||
"created_at": "2026-06-14T10:00:00Z"
|
||||
},
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 400 | `INVALID_INPUT` | 用户名/密码不符合校验规则 |
|
||||
| 409 | `USERNAME_TAKEN` | 用户名已存在 |
|
||||
|
||||
---
|
||||
|
||||
#### 1.2 登录
|
||||
|
||||
```
|
||||
POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```typescript
|
||||
interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应** `200 OK`:同 `AuthResponse` 结构。
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 400 | `INVALID_INPUT` | 请求参数缺失或格式错误 |
|
||||
| 401 | `INVALID_CREDENTIALS` | 用户名或密码错误 |
|
||||
|
||||
---
|
||||
|
||||
#### 1.3 刷新 Token
|
||||
|
||||
```
|
||||
POST /api/auth/refresh
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```typescript
|
||||
interface RefreshRequest {
|
||||
refresh_token: string; // 之前签发的 refresh_token
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应** `200 OK`:同 `AuthResponse` 结构(返回新的 access_token + refresh_token,旧 refresh_token 失效——Token 轮转)。
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 401 | `INVALID_TOKEN` | refresh_token 无效或已过期 |
|
||||
|
||||
---
|
||||
|
||||
#### 1.4 登出
|
||||
|
||||
```
|
||||
POST /api/auth/logout
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```typescript
|
||||
interface LogoutRequest {
|
||||
refresh_token: string; // 要废弃的 refresh_token
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应** `204 No Content`(无响应体)。
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 401 | `INVALID_TOKEN` | access_token 无效或已过期 |
|
||||
|
||||
---
|
||||
|
||||
### 二、对话接口(`/api/conversations`)
|
||||
|
||||
> 以下所有接口均需认证(`Authorization: Bearer <access_token>`),省略不重复标注。
|
||||
|
||||
#### 2.1 对话列表
|
||||
|
||||
```
|
||||
GET /api/conversations?page=1&size=20
|
||||
```
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `page` | int | 1 | 页码,从 1 开始 |
|
||||
| `size` | int | 20 | 每页条数,最大 50 |
|
||||
|
||||
**成功响应** `200 OK`:
|
||||
|
||||
```typescript
|
||||
interface ConversationListResponse {
|
||||
conversations: ConversationSummary[];
|
||||
total: number; // 总条数
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface ConversationSummary {
|
||||
id: string; // 对话 ID(即 session_id)
|
||||
title: string; // 对话标题(首条消息前 20 字)
|
||||
last_message: string; // 最后一条消息内容预览
|
||||
message_count: number; // 消息总数
|
||||
updated_at: string; // ISO 8601,最后活跃时间
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"conversations": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"title": "这是一朵红色的玫瑰…",
|
||||
"last_message": "它看起来很美丽。",
|
||||
"message_count": 4,
|
||||
"updated_at": "2026-06-14T10:05:30Z"
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"page": 1,
|
||||
"size": 20
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 401 | `INVALID_TOKEN` | 未认证或 token 过期 |
|
||||
|
||||
---
|
||||
|
||||
#### 2.2 创建对话
|
||||
|
||||
```
|
||||
POST /api/conversations
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**(可选,全部有默认值):
|
||||
|
||||
```typescript
|
||||
interface CreateConversationRequest {
|
||||
config?: {
|
||||
tts_enabled?: boolean; // 默认 true
|
||||
detail_level?: "low" | "high"; // 默认 "low"
|
||||
language?: string; // 默认 "zh-CN"
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应** `201 Created`:
|
||||
|
||||
```typescript
|
||||
interface ConversationDetail {
|
||||
id: string;
|
||||
title: string;
|
||||
config: {
|
||||
tts_enabled: boolean;
|
||||
detail_level: "low" | "high";
|
||||
language: string;
|
||||
};
|
||||
created_at: string; // ISO 8601
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "660e8400-e29b-41d4-a716-446655440001",
|
||||
"title": "新对话",
|
||||
"config": {
|
||||
"tts_enabled": true,
|
||||
"detail_level": "low",
|
||||
"language": "zh-CN"
|
||||
},
|
||||
"created_at": "2026-06-14T11:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 401 | `INVALID_TOKEN` | 未认证或 token 过期 |
|
||||
|
||||
---
|
||||
|
||||
#### 2.3 获取对话详情
|
||||
|
||||
```
|
||||
GET /api/conversations/:id
|
||||
```
|
||||
|
||||
**成功响应** `200 OK`:同 `ConversationDetail` 结构。
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 401 | `INVALID_TOKEN` | 未认证或 token 过期 |
|
||||
| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 |
|
||||
|
||||
---
|
||||
|
||||
#### 2.4 更新对话标题
|
||||
|
||||
```
|
||||
PATCH /api/conversations/:id
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
|
||||
```typescript
|
||||
interface UpdateTitleRequest {
|
||||
title: string; // 1-100 字符
|
||||
}
|
||||
```
|
||||
|
||||
**成功响应** `200 OK`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"title": "新的自定义标题"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 400 | `INVALID_INPUT` | title 为空或超长 |
|
||||
| 401 | `INVALID_TOKEN` | 未认证或 token 过期 |
|
||||
| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 |
|
||||
|
||||
---
|
||||
|
||||
#### 2.5 删除对话
|
||||
|
||||
```
|
||||
DELETE /api/conversations/:id
|
||||
```
|
||||
|
||||
**成功响应** `204 No Content`(无响应体)。
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 401 | `INVALID_TOKEN` | 未认证或 token 过期 |
|
||||
| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 |
|
||||
|
||||
---
|
||||
|
||||
#### 2.6 获取对话消息
|
||||
|
||||
```
|
||||
GET /api/conversations/:id/messages?limit=50&before=<message_id>
|
||||
```
|
||||
|
||||
**查询参数**:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `limit` | int | 50 | 返回条数,最大 100 |
|
||||
| `before` | int64 | — | 游标分页:返回此 message_id 之前的消息(不含),用于加载更多 |
|
||||
|
||||
**成功响应** `200 OK`:
|
||||
|
||||
```typescript
|
||||
interface MessagesResponse {
|
||||
messages: StoredMessage[];
|
||||
has_more: boolean; // 是否还有更早的消息
|
||||
}
|
||||
|
||||
interface StoredMessage {
|
||||
id: number; // 自增 ID,用于游标分页
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
tokens_used: number; // 该条消息消耗的 token 数
|
||||
created_at: string; // ISO 8601
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": 1001,
|
||||
"role": "user",
|
||||
"content": "这是什么花?",
|
||||
"tokens_used": 0,
|
||||
"created_at": "2026-06-14T10:01:00Z"
|
||||
},
|
||||
{
|
||||
"id": 1002,
|
||||
"role": "assistant",
|
||||
"content": "这是一朵红色的玫瑰。",
|
||||
"tokens_used": 42,
|
||||
"created_at": "2026-06-14T10:01:02Z"
|
||||
}
|
||||
],
|
||||
"has_more": false
|
||||
}
|
||||
```
|
||||
|
||||
**分页用法**:首次请求不带 `before`,获取最新消息。滚动到顶部时,取当前列表最小的 `id` 作为 `before` 参数请求更早的消息。
|
||||
|
||||
**错误响应**:
|
||||
|
||||
| 状态码 | code | 场景 |
|
||||
|--------|------|------|
|
||||
| 401 | `INVALID_TOKEN` | 未认证或 token 过期 |
|
||||
| 404 | `SESSION_NOT_FOUND` | 对话不存在或不属于当前用户 |
|
||||
|
||||
---
|
||||
|
||||
### 三、WebSocket 认证变更
|
||||
|
||||
连接地址变更为带 token 的查询参数:
|
||||
|
||||
```
|
||||
ws://localhost:8080/ws?token=<access_token>&conversation_id=<uuid>
|
||||
```
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `token` | 是 | JWT access_token |
|
||||
| `conversation_id` | 否 | 恢复已有对话;省略则创建新对话 |
|
||||
|
||||
**认证失败响应**(HTTP 升级前返回):
|
||||
|
||||
| 状态码 | 场景 |
|
||||
|--------|------|
|
||||
| 401 | token 缺失、无效或已过期 |
|
||||
|
||||
**conversation_id 校验失败**:
|
||||
|
||||
| 场景 | 处理 |
|
||||
|------|------|
|
||||
| 对话不存在 | 返回 401,`{"error": "SESSION_NOT_FOUND"}` |
|
||||
| 对话不属于当前用户 | 返回 401,`{"error": "SESSION_NOT_FOUND"}`(与不存在相同,避免信息泄露) |
|
||||
|
||||
**连接成功后**:`connected` 消息不变,新增 `conversation_id` 字段标识当前对话:
|
||||
|
||||
```typescript
|
||||
interface ConnectedMessage {
|
||||
type: "connected";
|
||||
session_id: string; // 对话 ID
|
||||
conversation_id: string; // 同 session_id,便于前端统一使用
|
||||
server_version: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 四、前端调用示例
|
||||
|
||||
#### 认证状态管理
|
||||
|
||||
```typescript
|
||||
// 存储 token(建议 localStorage 或内存,视安全需求)
|
||||
interface AuthTokens {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
// 请求拦截器:自动附加 Authorization 头
|
||||
async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
const tokens = getStoredTokens();
|
||||
const headers = {
|
||||
...options.headers,
|
||||
"Authorization": `Bearer ${tokens.accessToken}`,
|
||||
};
|
||||
|
||||
let resp = await fetch(url, { ...options, headers });
|
||||
|
||||
// 401 时尝试刷新 token
|
||||
if (resp.status === 401 && tokens.refreshToken) {
|
||||
const refreshResp = await fetch("/api/auth/refresh", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refresh_token: tokens.refreshToken }),
|
||||
});
|
||||
|
||||
if (refreshResp.ok) {
|
||||
const newTokens: AuthResponse = await refreshResp.json();
|
||||
storeTokens({
|
||||
accessToken: newTokens.access_token,
|
||||
refreshToken: newTokens.refresh_token,
|
||||
});
|
||||
// 用新 token 重试原请求
|
||||
headers["Authorization"] = `Bearer ${newTokens.access_token}`;
|
||||
resp = await fetch(url, { ...options, headers });
|
||||
} else {
|
||||
// refresh 也失败,跳转登录
|
||||
redirectToLogin();
|
||||
}
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
```
|
||||
|
||||
#### 注册 + 登录
|
||||
|
||||
```typescript
|
||||
async function register(username: string, password: string): Promise<AuthResponse> {
|
||||
const resp = await fetch("/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const err: ApiError = await resp.json();
|
||||
throw new Error(err.message); // "username already taken" 等
|
||||
}
|
||||
|
||||
return resp.json();
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取对话列表
|
||||
|
||||
```typescript
|
||||
async function getConversations(page = 1, size = 20): Promise<ConversationListResponse> {
|
||||
const resp = await authFetch(
|
||||
`/api/conversations?page=${page}&size=${size}`
|
||||
);
|
||||
if (!resp.ok) throw new Error("Failed to load conversations");
|
||||
return resp.json();
|
||||
}
|
||||
```
|
||||
|
||||
#### 加载对话历史消息
|
||||
|
||||
```typescript
|
||||
async function getMessages(
|
||||
conversationId: string,
|
||||
limit = 50,
|
||||
before?: number
|
||||
): Promise<MessagesResponse> {
|
||||
let url = `/api/conversations/${conversationId}/messages?limit=${limit}`;
|
||||
if (before !== undefined) url += `&before=${before}`;
|
||||
|
||||
const resp = await authFetch(url);
|
||||
if (!resp.ok) throw new Error("Failed to load messages");
|
||||
return resp.json();
|
||||
}
|
||||
```
|
||||
|
||||
#### 建立 WebSocket 连接(带认证)
|
||||
|
||||
```typescript
|
||||
function connectWebSocket(accessToken: string, conversationId?: string): WebSocket {
|
||||
let url = `/ws?token=${encodeURIComponent(accessToken)}`;
|
||||
if (conversationId) {
|
||||
url += `&conversation_id=${encodeURIComponent(conversationId)}`;
|
||||
}
|
||||
return new WebSocket(url);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键文件清单
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user