Files
Jog/doc/AI助手模型管理功能技术实施文档.md
T

647 lines
23 KiB
Markdown
Raw Normal View History

2026-05-24 00:39:10 +08:00
# AI 助手模型管理功能技术实施文档
| 文档版本 | 修订日期 | 修订人 | 修订内容 |
| :------- | :--------- | :----- | :------------------------------------------------- |
| V1.0 | 2026-05-23 | — | 基于《AI助手模型管理功能需求》V1.1 初稿 |
| V1.1 | 2026-05-23 | — | 补充与润色、标签推荐等功能的实施边界说明 |
**关联需求**[AI助手模型管理功能需求.md](./AI助手模型管理功能需求.md)V1.2
**依赖能力**[AI助手功能技术实施文档.md](./AI助手功能技术实施文档.md)AI 助手 V1.1 已落地)
**与其它 AI 功能(实施边界)**:本期 **不修改** `TextPolishServiceImpl``TagRecommendServiceImpl``AiSummaryServiceImpl``properties.getModel()` 逻辑;润色、标签推荐、摘要仍走 `dashscope.model`。仅改造 `AiChatServiceImpl` 及新增 `AiAssistantModel*` 相关类。
---
## 前提条件
**数据安全规则**(与项目一致):
- 联调产生的测试模型、会话,完成后可清理;勿动名为 **「hello」** 的文章
**环境依赖**
- AI 助手相关表已存在(Flyway `V5__ai_chat_tables.sql`
- `DASHSCOPE_API_KEY` 已配置
- MySQL、Redis 可用
**产品约定(实施须遵守)**
- **D-08**:会话创建时绑定 `model_id`,同会话不换模
- **D-09**:模型 `enabled=0` 后,绑定会话 **仅可查看**`POST /chat` 拒绝
- **D-10**:摘要 / 润色 / 标签推荐 **不改**,仍用 `DashScopeProperties.model`
- **D-13**`allow_image` / `allow_voice` 仅管理端配置 + API 返回;用户端 **无** 上传 UI
---
## 1. 功能概述
在现有 AI 助手基础上增加:
| 端 | 能力 |
| :----- | :------------------------------------------------------------------- |
| 管理后台 | `/admin/ai-models` 模型 CRUD(逻辑删除)、默认模型、启用/禁用、多模态开关 |
| 用户端 | 助手页模型下拉;`localStorage` 记住选择;新会话绑定模型 |
| 后端 | 表 `ai_assistant_model``ai_chat_session.model_id`;按 `model_code` 调 DashScope |
**与现网差异**`AiChatServiceImpl.buildRequestBody``"model", properties.getModel()` 改为会话绑定模型的 `model_code`
---
## 2. 核心实现思路
### 2.1 复用项
| 复用项 | 说明 |
| :----- | :--- |
| `AiSummaryServiceImpl.callModel` | 仍负责 HTTP 调用 DashScope,仅请求体 `model` 字段来源变化 |
| `DashScopeProperties` | `api-key``base-url``timeout``max-input``model` 作种子与异常兜底 |
| `AiChatServiceImpl` | 在现有 chat / session 流程上扩展,不重写 |
| 管理后台模式 | 参考 `AdminCategoryController` + `Categories.vue` 表格 + 弹窗 |
| `@TableLogic` | `ai_assistant_model.deleted``ai_chat_session.deleted` 一致 |
### 2.2 新增组件关系
```mermaid
flowchart LR
subgraph admin [管理端]
AiModelsVue --> aiModelApi
aiModelApi --> AdminAiModelController
end
subgraph app [用户端]
AiAssistantVue --> aiModelApi
AiAssistantVue --> aiChatApi
aiChatApi --> AiChatController
end
AdminAiModelController --> AiAssistantModelService
AiChatController --> AiChatService
AiChatService --> AiAssistantModelService
AiAssistantModelService --> AiAssistantModelMapper
AiChatService --> AiChatSessionMapper
```
### 2.3 模型解析与只读(核心)
```
createSession(userId, modelId?)
→ resolveModelForNewSession(modelId) // 启用且未删;否则默认模型
→ insert session.model_id
chat(userId, dto)
→ session = getSessionForUser(...)
→ model = getModelById(session.model_id) // 含已禁用、已删记录(历史需要)
→ if model == null || model.deleted → 400
→ if !model.enabled → throw AI_MODEL_DISABLED
→ buildRequestBody(..., model.modelCode)
```
**只读原因**`readOnlyReason`):
| 值 | 条件 |
| :--- | :--- |
| `null` | 可继续对话 |
| `ROUND_FULL` | `round_count >= 5` |
| `MODEL_DISABLED` | 绑定模型 `enabled = 0`(优先于 ROUND_FULL 展示文案时,前端 `modelEnabled === false` 时用停用提示) |
---
## 3. 改动项清单
### 3.1 数据库(Flyway
**文件**`jog-admin/src/main/resources/db/migration/V6__ai_assistant_model.sql`
(当前仓库最大版本为 V5,故使用 V6。)
```sql
-- V6__ai_assistant_model.sql
-- AI 助手:可配置模型 + 会话绑定
CREATE TABLE ai_assistant_model (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
model_code VARCHAR(64) NOT NULL COMMENT 'DashScope model 参数',
display_name VARCHAR(32) NOT NULL,
description VARCHAR(200) NULL,
enabled TINYINT NOT NULL DEFAULT 1,
allow_image TINYINT NOT NULL DEFAULT 0,
allow_voice TINYINT NOT NULL DEFAULT 0,
is_default TINYINT NOT NULL DEFAULT 0,
sort INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT NOT NULL DEFAULT 0,
INDEX idx_enabled_sort (enabled, sort, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 种子:与 application.yml 默认 qwen-turbo 对齐;部署若 DASHSCOPE_MODEL 不同,上线后可在后台改 model_code
INSERT INTO ai_assistant_model (
model_code, display_name, description, enabled, allow_image, allow_voice, is_default, sort
) VALUES (
'qwen-turbo', '默认模型', '系统迁移种子模型', 1, 0, 0, 1, 0
);
ALTER TABLE ai_chat_session
ADD COLUMN model_id BIGINT NULL COMMENT '绑定 ai_assistant_model.id' AFTER user_id;
UPDATE ai_chat_session SET model_id = (SELECT id FROM ai_assistant_model WHERE is_default = 1 AND deleted = 0 LIMIT 1)
WHERE model_id IS NULL;
```
> **注意**`model_code` 唯一性由应用层在 `deleted=0` 范围内校验,不在 DB 建唯一索引(避免软删后无法复用编码)。
---
### 3.2 后端实体与 Mapper
| 文件 | 说明 |
| :--- | :--- |
| `entity/AiAssistantModel.java` | `@TableName("ai_assistant_model")``@TableLogic` on `deleted` |
| `mapper/AiAssistantModelMapper.java` | `BaseMapper<AiAssistantModel>` |
**实体字段映射**Java 侧建议布尔包装,MyBatis 映射 TINYINT):
```java
@Data
@TableName("ai_assistant_model")
public class AiAssistantModel {
@TableId(type = IdType.AUTO)
private Long id;
private String modelCode;
private String displayName;
private String description;
private Boolean enabled;
private Boolean allowImage;
private Boolean allowVoice;
private Boolean isDefault;
private Integer sort;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedAt;
@TableLogic
private Integer deleted;
}
```
**修改** `entity/AiChatSession.java`:增加 `private Long modelId;`
---
### 3.3 后端 DTO / VO
| 文件 | 说明 |
| :--- | :--- |
| `dto/AiAssistantModelSaveDTO.java` | 管理端新增/修改:`modelCode, displayName, description, enabled, allowImageUpload, allowVoiceUpload, isDefault, sort` + Jakarta 校验 |
| `dto/AiAssistantModelVO.java` | 管理端列表/详情(含 `modelCode` |
| `dto/AiAssistantModelAppVO.java` | 用户端列表(**无** `modelCode` |
| `dto/CreateAiChatSessionDTO.java` | 可选 `modelId` |
| `dto/AiChatSessionMessagesVO.java` | 消息列表包装:`messages, modelEnabled, readOnlyReason, roundCount, maxRounds` |
| `dto/CreateAiChatSessionDTO.java` | `@RequestBody(required = false)` 用于 POST sessions |
**扩展已有 VO**
```java
// AiChatSessionVO 增加
private Long modelId;
private String modelDisplayName;
private Boolean modelEnabled;
// AiChatResultVO 增加(可选)
private String modelDisplayName;
// AiChatSendDTO 增加(可选,会话级绑定后 chat 内忽略)
private Long modelId;
```
**枚举** `enums/AiChatReadOnlyReason.java`
```java
public enum AiChatReadOnlyReason {
ROUND_FULL,
MODEL_DISABLED
}
```
---
### 3.4 后端 Service
#### 3.4.1 `AiAssistantModelService` / `AiAssistantModelServiceImpl`
| 方法 | 说明 |
| :--- | :--- |
| `List<AiAssistantModelVO> listAllForAdmin()` | `deleted=0``ORDER BY sort ASC, id ASC` |
| `AiAssistantModelVO getByIdForAdmin(Long id)` | 不存在 → `NOT_FOUND` |
| `AiAssistantModelVO create(AiAssistantModelSaveDTO dto)` | 校验编码唯一、默认模型互斥 |
| `void update(Long id, AiAssistantModelSaveDTO dto)` | 禁用前 `assertAtLeastOneEnabled`;设默认时清除其它 `is_default` |
| `void deleteLogical(Long id)` | 禁止删当前默认;删后仍须至少一条 `enabled`(若删的是启用项) |
| `List<AiAssistantModelAppVO> listEnabledForApp()` | `enabled=1``deleted=0` |
| `AiAssistantModel getById(Long id)` | 含已删(`@TableLogic` 需用自定义查询或 `selectById` 带逻辑) |
| `AiAssistantModel requireEnabledById(Long id)` | 创建会话用 |
| `AiAssistantModel getDefaultModel()` | `is_default=1 AND deleted=0`;无则 warn + 合成 fallback`dashscope.model` |
**默认模型切换**(事务内):
```java
@Transactional
public void applyDefaultFlag(Long modelId, boolean isDefault) {
if (isDefault) {
modelMapper.update(null, Wrappers.<AiAssistantModel>lambdaUpdate()
.set(AiAssistantModel::getIsDefault, false)
.eq(AiAssistantModel::getDeleted, 0));
}
// 再 update 目标行 is_default
}
```
**编码唯一**
```java
long count = modelMapper.selectCount(Wrappers.<AiAssistantModel>lambdaQuery()
.eq(AiAssistantModel::getModelCode, code)
.ne(excludeId != null, AiAssistantModel::getId, excludeId));
if (count > 0) throw new BusinessException(ErrorCode.AI_MODEL_CODE_DUPLICATE);
```
#### 3.4.2 修改 `AiChatService` / `AiChatServiceImpl`
| 变更点 | 说明 |
| :----- | :--- |
| `createSession(Long userId, Long modelId)` | 解析模型 → `session.setModelId(...)` |
| `listSessions` | `toSessionVO` 联查或批量查模型,填充 `modelDisplayName``modelEnabled` |
| `listMessages` | 返回类型改为 `AiChatSessionMessagesVO`;计算 `readOnlyReason` |
| `chat` | 开头校验绑定模型 `enabled``buildRequestBody(history, modelCode)` |
| 日志 | `log.info("AI 助手成功 sessionId={}, modelId={}, modelCode={}, ...")` |
**禁用校验**`chat` 内,在轮次校验之后):
```java
AiAssistantModel bound = aiAssistantModelService.getById(session.getModelId());
if (bound == null || Boolean.FALSE.equals(bound.getEnabled())) {
throw new BusinessException(ErrorCode.AI_MODEL_DISABLED);
}
```
**`buildRequestBody` 签名**
```java
private Map<String, Object> buildRequestBody(List<AiChatMessage> history, String modelCode) {
// ...
return Map.of(
"model", modelCode,
"messages", messages,
"temperature", 0.7,
"top_p", 0.9
);
}
```
**`resolveReadOnlyReason`**
```java
private AiChatReadOnlyReason resolveReadOnlyReason(AiChatSession session, AiAssistantModel model) {
if (model != null && Boolean.FALSE.equals(model.getEnabled())) {
return AiChatReadOnlyReason.MODEL_DISABLED;
}
if (session.getRoundCount() != null && session.getRoundCount() >= MAX_ROUNDS) {
return AiChatReadOnlyReason.ROUND_FULL;
}
return null;
}
```
> 已删除模型:`getById` 若被 `@TableLogic` 过滤为 null,展示 `modelDisplayName = "已下架模型"``modelEnabled = false``readOnlyReason = MODEL_DISABLED`,且 `chat` 拒绝。
---
### 3.5 后端 Controller
#### 3.5.1 `AdminAiModelController.java`
```java
@Tag(name = "管理员-AI 助手模型")
@RestController
@RequestMapping("/api/admin/ai/models")
@RequiredArgsConstructor
public class AdminAiModelController {
private final AiAssistantModelService modelService;
@GetMapping
public ApiResult<List<AiAssistantModelVO>> list() { ... }
@GetMapping("/{id}")
public ApiResult<AiAssistantModelVO> get(@PathVariable Long id) { ... }
@PostMapping
public ApiResult<AiAssistantModelVO> create(@Valid @RequestBody AiAssistantModelSaveDTO dto) { ... }
@PutMapping("/{id}")
public ApiResult<Void> update(@PathVariable Long id, @Valid @RequestBody AiAssistantModelSaveDTO dto) { ... }
@DeleteMapping("/{id}")
public ApiResult<Void> delete(@PathVariable Long id) { ... }
}
```
权限:沿用 Spring Security 对 `/api/admin/**` 的 ADMIN 规则(与 `CategoryController` 一致)。
#### 3.5.2 扩展 `AiChatController.java`
```java
@GetMapping("/models")
public ApiResult<List<AiAssistantModelAppVO>> listModels(@AuthenticationPrincipal Long userId) {
return ApiResult.success(aiAssistantModelService.listEnabledForApp());
}
@PostMapping("/sessions")
public ApiResult<CreateAiChatSessionVO> createSession(
@AuthenticationPrincipal Long userId,
@RequestBody(required = false) CreateAiChatSessionDTO body) {
Long modelId = body == null ? null : body.getModelId();
return ApiResult.success(aiChatService.createSession(userId, modelId));
}
@GetMapping("/sessions/{sessionId}/messages")
public ApiResult<AiChatSessionMessagesVO> listMessages(...) { ... }
```
---
### 3.6 错误码
`jog-common/.../ErrorCode.java` 新增(code 段延续 500x):
| 枚举 | code | message(默认) |
| :--- | :--- | :--- |
| `AI_MODEL_CODE_DUPLICATE` | 5007 | 模型编码已存在 |
| `AI_MODEL_NOT_AVAILABLE` | 5008 | 模型不可用 |
| `AI_MODEL_DISABLED` | 5009 | 该会话所用模型已停用,仅可查看历史 |
| `AI_MODEL_MUST_KEEP_ENABLED` | 5010 | 须至少保留一个启用的模型 |
| `AI_MODEL_DEFAULT_REQUIRED` | 5011 | 请先指定其它模型为默认后再删除 |
`BusinessException` 可覆盖 message 以贴合 FR-AI-MODEL-ADM-07/08 文案。
---
### 3.7 前端改动
#### 3.7.1 API
**新建** `jog-frontend/src/api/aiModel.js`
```js
import request from '@/utils/request'
// 管理端
export function listAdminAiModels() {
return request.get('/api/admin/ai/models')
}
export function createAdminAiModel(data) {
return request.post('/api/admin/ai/models', data)
}
export function updateAdminAiModel(id, data) {
return request.put(`/api/admin/ai/models/${id}`, data)
}
export function deleteAdminAiModel(id) {
return request.delete(`/api/admin/ai/models/${id}`)
}
// 用户端
export function listAppAiModels() {
return request.get('/api/app/ai/models')
}
```
**修改** `jog-frontend/src/api/aiChat.js`
```js
export function createAiChatSession(data) {
return request.post('/api/app/ai/sessions', data ?? {})
}
export function listAiChatMessages(sessionId) {
// 响应 data 结构变为 { messages, modelEnabled, readOnlyReason, roundCount, maxRounds }
return request.get(`/api/app/ai/sessions/${sessionId}/messages`)
}
```
#### 3.7.2 管理后台 `views/admin/AiModels.vue`
- 参考 `Categories.vue``el-table` + `el-dialog` 表单
- 表单字段:展示名称、模型编码、描述、启用、**允许上传图片**、**允许上传语音**、默认、排序
- 列表列:ID、展示名称、编码、状态、图片、语音、默认、排序、操作
- 删除:`ElMessageBox.confirm``deleteAdminAiModel`
- **不实现** 任何上传控件(D-13)
#### 3.7.3 路由与菜单
| 文件 | 变更 |
| :--- | :--- |
| `router/index.js` | `children` 增加 `{ path: 'ai-models', name: 'AdminAiModels', component: ..., meta: { title: 'AI 模型', requiresAdmin: true } }` |
| `layouts/AdminLayout.vue` | `el-menu-item index="/admin/ai-models"` 文案「AI 模型」 |
#### 3.7.4 用户端 `AiAssistant.vue` + `AiChatPanel.vue`
**状态新增**
```js
const MODEL_STORAGE_KEY = 'jog_ai_assistant_model_id'
const availableModels = ref([])
const selectedModelId = ref(null)
const modelEnabled = ref(true)
const readOnlyReason = ref(null) // 'ROUND_FULL' | 'MODEL_DISABLED' | null
```
**`onMounted`**
```js
await loadAppModels()
restoreModelIdFromStorage()
// 原有 loadSessions + selectSession
```
**`readOnly` 计算**
```js
const readOnly = computed(() =>
roundCount.value >= maxRounds || modelEnabled.value === false
)
```
**`selectSession`**:解析 `listAiChatMessages` 新结构:
```js
const res = await listAiChatMessages(item.id)
const payload = res.data
messages.value = payload.messages ?? []
modelEnabled.value = payload.modelEnabled !== false
readOnlyReason.value = payload.readOnlyReason ?? null
roundCount.value = payload.roundCount ?? item.roundCount ?? 0
```
**`handleNewSession`**
```js
const res = await createAiChatSession({ modelId: selectedModelId.value })
```
**`AiChatPanel` 扩展**
- Props`selectedModelId``availableModels``readOnlyReason`
- 标题栏:`el-select` 绑定 `selectedModelId``@change``localStorage`**不** 改当前 session
- 提示条:`readOnlyReason === 'MODEL_DISABLED'` 时显示停用文案;`ROUND_FULL` 沿用现有满 5 轮条(可合并为两个 `v-if`
**不实现**:图片/语音上传按钮(即使 `allowImageUpload=true` 也不展示)。
---
## 4. 核心逻辑说明
### 4.1 创建会话时模型绑定
```mermaid
sequenceDiagram
participant FE as 前端
participant BE as AiChatService
participant MS as AiAssistantModelService
FE->>BE: POST /sessions { modelId: 2 }
BE->>MS: requireEnabledById(2) 或 getDefaultModel()
MS-->>BE: model(id=2, modelCode=qwen-plus)
BE->>BE: insert session(model_id=2)
BE-->>FE: { sessionId }
```
### 4.2 禁用后只读
```mermaid
sequenceDiagram
participant U as 用户
participant FE as 前端
participant BE as 后端
U->>FE: 点击绑定已禁用模型的会话
FE->>BE: GET /sessions/{id}/messages
BE-->>FE: modelEnabled=false, readOnlyReason=MODEL_DISABLED
FE->>FE: 禁用输入框 + 提示条
U->>FE: 尝试发送
FE->>FE: readOnly 拦截(可选)
FE->>BE: POST /chat
BE-->>FE: 400 AI_MODEL_DISABLED
```
### 4.3 与多模态字段的关系(本期)
- 管理端保存 `allowImage` / `allowVoice` 至 DB
- `GET /api/app/ai/models` 返回两字段,前端 **不消费**
- `POST /chat` **无** `attachments` 字段;后续迭代在 Service 内校验 `allowImage`
---
## 5. 测试要点
### 5.1 后端单元测试
**新建** `AiAssistantModelServiceImplTest`Mockito):
| 用例 | 预期 |
| :--- | :--- |
| 创建重复 modelCode | `AI_MODEL_CODE_DUPLICATE` |
| 设 B 为默认 | 仅 B `isDefault=true` |
| 禁用最后一个启用模型 | `AI_MODEL_MUST_KEEP_ENABLED` |
| 删除默认模型 | `AI_MODEL_DEFAULT_REQUIRED` |
**扩展** `AiChatServiceImplTest`
| 用例 | 预期 |
| :--- | :--- |
| createSession 带 modelId | `session.modelId` 正确 |
| chat 使用绑定 modelCode | `callModel` 参数含对应 model |
| 绑定模型 enabled=false | `AI_MODEL_DISABLED` |
| listMessages | `readOnlyReason=MODEL_DISABLED` |
**扩展** `AiChatServiceIntegrationTest`:覆盖消息包装 VO。
### 5.2 前端手工验收
对照需求 AC-AI-MODEL-ADM-*、AC-AI-MODEL-FE-*、AC-AI-MODEL-BE-*。
### 5.3 联调检查清单
- [ ] Flyway V6 执行成功;旧 `ai_chat_session` 均有 `model_id`
- [ ] 管理端 CRUD、默认模型互斥、禁止删唯一启用
- [ ] 用户下拉仅启用模型;`localStorage` 恢复有效
- [ ] 新会话绑定所选模型;同会话中途改下拉 **不** 换模
- [ ] 禁用模型后旧会话只读、`POST /chat` 400
- [ ] 助手页 **无** 图片/语音上传入口
- [ ] 摘要/润色仍走 `dashscope.model`(回归一条即可)
---
## 6. 待办事项
> **状态说明**:⬜ 未开始 🔄 进行中 ✅ 已完成
| 序号 | 阶段 | 待办项 | 状态 |
| :--- | :--- | :----- | :--- |
| 1 | 数据库 | 新增 `V6__ai_assistant_model.sql`(建表、种子、session.model_id 回填) | ⬜ 未开始 |
| 2 | 后端 | 实体 `AiAssistantModel``AiChatSession.modelId` | ⬜ 未开始 |
| 3 | 后端 | `AiAssistantModelMapper` | ⬜ 未开始 |
| 4 | 后端 | DTO/VO`AiAssistantModelSaveDTO``AiAssistantModelVO``AiAssistantModelAppVO``AiChatSessionMessagesVO``CreateAiChatSessionDTO` | ⬜ 未开始 |
| 5 | 后端 | `AiChatReadOnlyReason` 枚举 | ⬜ 未开始 |
| 6 | 后端 | `ErrorCode` 新增 5007~5011 | ⬜ 未开始 |
| 7 | 后端 | `AiAssistantModelService` + `Impl`(CRUD、默认、校验) | ⬜ 未开始 |
| 8 | 后端 | `AdminAiModelController` | ⬜ 未开始 |
| 9 | 后端 | `AiChatController` 增加 `GET /models`sessions 支持 body | ⬜ 未开始 |
| 10 | 后端 | `AiChatServiceImpl` 改造(绑定、禁用校验、modelCode、消息包装) | ⬜ 未开始 |
| 11 | 后端 | 单元/集成测试 | ⬜ 未开始 |
| 12 | 前端 | `api/aiModel.js` | ⬜ 未开始 |
| 13 | 前端 | `views/admin/AiModels.vue` | ⬜ 未开始 |
| 14 | 前端 | `AdminLayout` + `router` 注册 `/admin/ai-models` | ⬜ 未开始 |
| 15 | 前端 | `api/aiChat.js` 适配新 sessions/messages 结构 | ⬜ 未开始 |
| 16 | 前端 | `AiAssistant.vue` 模型列表、localStorage、readOnly 逻辑 | ⬜ 未开始 |
| 17 | 前端 | `AiChatPanel.vue` 模型选择器 + 停用提示条 | ⬜ 未开始 |
| 18 | 联调 | 管理端上架第二模型 → 用户新会话切换 → 对话成功 | ⬜ 未开始 |
| 19 | 联调 | 禁用模型 → 旧会话只读 + chat 400 | ⬜ 未开始 |
| 20 | 验收 | 按需求 V1.1 验收标准逐项确认 | ⬜ 未开始 |
---
## 7. 实施顺序建议
1. **Flyway V6** → 本地启动验证表与种子、session 回填
2. **模型 Service + Admin Controller** → Postman 测通 CRUD
3. **AiChatService 改造** → 单元测试 → 与前端联调 sessions/chat/messages
4. **管理后台页面** → 运营可配模型
5. **助手页选择器 + 只读** → 禁用场景联调
6. **回归** 原 AI 助手 5 轮、重试、限流不受影响
---
## 8. 风险与注意事项
| 风险 | 说明 | 缓解 |
| :--- | :--- | :--- |
| 种子 model_code 与环境不一致 | SQL 写死 `qwen-turbo` | 上线后管理端改编码或重建默认行 |
| `listMessages` 破坏性变更 | 由 `List` 改为包装对象 | 前后端同 PR 发布;仅 AI 助手页消费 |
| 逻辑删模型后 session.model_id 悬空 | 展示「已下架」 | `getById` 自定义查询含 deleted 或冗余 displayName 快照(首版联查+兜底文案) |
| 禁用与满 5 轮双重只读 | 文案冲突 | 前端优先展示 `MODEL_DISABLED` |
| 误开上传 UI | 违背 D-13 | Code Review 确认 Panel 无 upload 组件 |
---
## 9. 需求追溯矩阵(摘要)
| 需求编号 | 实施落点 |
| :------- | :------- |
| FR-AI-MODEL-ADM-* | 待办 7、8、13、14 |
| FR-AI-MODEL-FE-* | 待办 15~17 |
| FR-AI-MODEL-BE-* | 待办 1~11 |
| D-08 ~ D-13 | 待办 10、16~19 |
| NFR-02 ~ NFR-05 | 待办 6、8、10、11 |
---
**编写人:** AI 助手
**评审人:** 待评审
**生效日期:** 2026-05-23