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:
hhs
2026-06-14 18:05:50 +08:00
parent c3a32ce276
commit b57adf153b
2 changed files with 1157 additions and 48 deletions

View File

@@ -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; // JWT15 分钟有效
refresh_token: string; // JWT7 天有效
}
```
```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);
}
```
---
## 关键文件清单
```