docs: refresh frontend integration notes
This commit is contained in:
@@ -1,529 +0,0 @@
|
|||||||
# RBAC 路由权限 API 响应示例 v3.2
|
|
||||||
|
|
||||||
## 📋 文档说明
|
|
||||||
|
|
||||||
本文档展示 v3.2 版本 RBAC 路由权限接口的完整响应格式。
|
|
||||||
|
|
||||||
**v3.2 核心变更**:
|
|
||||||
- ✅ 使用 `status` 字段管理路由启用/禁用状态
|
|
||||||
- ✅ 取消勾选后路由不删除,只是设置为 status=0
|
|
||||||
- ✅ 接口返回 `enabled` 字段标识路由是否启用
|
|
||||||
- ✅ 仅省级管理员可修改路由权限
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1️⃣ GET /rbac/roles/{role_id}/routes
|
|
||||||
|
|
||||||
### 功能说明
|
|
||||||
获取角色关联的所有路由(包括禁用的),每个路由带 `enabled` 字段
|
|
||||||
|
|
||||||
### 请求示例
|
|
||||||
```bash
|
|
||||||
GET /rbac/roles/1/routes
|
|
||||||
Authorization: Bearer <token>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 响应示例
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"role_id": 1,
|
|
||||||
"routes": [
|
|
||||||
{
|
|
||||||
"id": 31,
|
|
||||||
"route_path": "/home",
|
|
||||||
"route_name": "Home",
|
|
||||||
"route_title": "系统概览",
|
|
||||||
"parent_id": null,
|
|
||||||
"icon": "ri-dashboard-line",
|
|
||||||
"sort_order": 1,
|
|
||||||
"is_hidden": false,
|
|
||||||
"component": "views/Home.vue",
|
|
||||||
"enabled": true,
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"id": 10,
|
|
||||||
"permission_key": "dashboard:stats:view",
|
|
||||||
"display_name": "查看统计数据",
|
|
||||||
"api_method": "GET",
|
|
||||||
"api_path": "/api/v3/dashboard/stats"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"route_path": "/documents",
|
|
||||||
"route_name": "Documents",
|
|
||||||
"route_title": "文件管理",
|
|
||||||
"parent_id": null,
|
|
||||||
"icon": "ri-folder-3-line",
|
|
||||||
"sort_order": 2,
|
|
||||||
"is_hidden": false,
|
|
||||||
"component": "views/documents/Index.vue",
|
|
||||||
"enabled": false,
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"id": 20,
|
|
||||||
"permission_key": "document:list:read",
|
|
||||||
"display_name": "查看文档列表",
|
|
||||||
"api_method": "GET",
|
|
||||||
"api_path": "/api/v2/documents"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": 59,
|
|
||||||
"route_path": "/files/upload",
|
|
||||||
"route_name": "FilesUpload",
|
|
||||||
"route_title": "文件上传",
|
|
||||||
"parent_id": 2,
|
|
||||||
"enabled": false,
|
|
||||||
"permissions": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 41,
|
|
||||||
"route_path": "/rules",
|
|
||||||
"route_name": "Rules",
|
|
||||||
"route_title": "评查规则库",
|
|
||||||
"parent_id": null,
|
|
||||||
"icon": "ri-book-3-line",
|
|
||||||
"sort_order": 3,
|
|
||||||
"is_hidden": false,
|
|
||||||
"component": "views/rules/Index.vue",
|
|
||||||
"enabled": true,
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"id": 28,
|
|
||||||
"permission_key": "evaluation_group:list:read",
|
|
||||||
"display_name": "查看评查点分组列表",
|
|
||||||
"api_method": "GET",
|
|
||||||
"api_path": "/api/v3/evaluation-point-groups"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 字段说明
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `enabled` | boolean | **true=启用(前端显示为勾选)**, **false=禁用(前端显示为未勾选)** |
|
|
||||||
| `permissions` | array | 路由关联的API权限列表 |
|
|
||||||
|
|
||||||
### 前端使用示例
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 获取角色路由
|
|
||||||
const response = await fetch('/rbac/roles/1/routes')
|
|
||||||
const { routes } = response.data
|
|
||||||
|
|
||||||
// 提取启用的路由ID(用于Tree组件初始勾选)
|
|
||||||
const checkedKeys = []
|
|
||||||
const extractEnabled = (routeList) => {
|
|
||||||
routeList.forEach(route => {
|
|
||||||
if (route.enabled) {
|
|
||||||
checkedKeys.push(route.id)
|
|
||||||
}
|
|
||||||
if (route.children) {
|
|
||||||
extractEnabled(route.children)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
extractEnabled(routes)
|
|
||||||
|
|
||||||
// 设置Tree组件默认勾选
|
|
||||||
treeRef.value.setCheckedKeys(checkedKeys)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2️⃣ PUT /rbac/roles/{role_id}/routes
|
|
||||||
|
|
||||||
### 功能说明
|
|
||||||
批量更新角色路由权限(状态管理模式)
|
|
||||||
- route_ids 中的路由设置为启用(status=1)
|
|
||||||
- 不在 route_ids 中的路由设置为禁用(status=0)
|
|
||||||
- **仅省级管理员可调用**
|
|
||||||
|
|
||||||
### 请求示例
|
|
||||||
```bash
|
|
||||||
PUT /rbac/roles/1/routes
|
|
||||||
Authorization: Bearer <provincial_admin_token>
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"route_ids": [31, 41],
|
|
||||||
"permission": "RW"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 响应示例(成功)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 200,
|
|
||||||
"msg": "success",
|
|
||||||
"data": {
|
|
||||||
"role_id": 1,
|
|
||||||
"enabled_count": 2,
|
|
||||||
"disabled_count": 1,
|
|
||||||
"inserted_count": 0,
|
|
||||||
"route_ids": [31, 41]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 响应字段说明
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `enabled_count` | int | 本次启用的路由数量 |
|
|
||||||
| `disabled_count` | int | 本次禁用的路由数量 |
|
|
||||||
| `inserted_count` | int | 本次新插入的路由数量 |
|
|
||||||
| `route_ids` | array | 启用的路由ID列表 |
|
|
||||||
|
|
||||||
### 响应示例(权限不足)
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 4003,
|
|
||||||
"msg": "权限不足:仅省级管理员可以修改角色路由权限",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 前端使用示例
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 保存路由权限
|
|
||||||
const saveRoutePermissions = async (roleId) => {
|
|
||||||
const checkedKeys = treeRef.value.getCheckedKeys()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/rbac/roles/${roleId}/routes`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
route_ids: checkedKeys,
|
|
||||||
permission: 'RW'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
if (result.code === 200) {
|
|
||||||
ElMessage.success(
|
|
||||||
`成功启用 ${result.data.enabled_count} 个路由,` +
|
|
||||||
`禁用 ${result.data.disabled_count} 个路由`
|
|
||||||
)
|
|
||||||
|
|
||||||
// 刷新路由列表以更新enabled状态
|
|
||||||
await loadRoutes()
|
|
||||||
} else if (result.code === 4003) {
|
|
||||||
ElMessage.error('权限不足:仅省级管理员可以修改路由权限')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('保存失败:' + error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3️⃣ 完整工作流程示例
|
|
||||||
|
|
||||||
### Vue 3 + Element Plus 完整组件
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<template>
|
|
||||||
<div class="role-route-manager">
|
|
||||||
<el-card>
|
|
||||||
<template #header>
|
|
||||||
<div class="card-header">
|
|
||||||
<span>角色路由权限管理 - {{ roleName }}</span>
|
|
||||||
<el-tag v-if="!isProvincialAdmin" type="warning">
|
|
||||||
仅查看模式(需要省级管理员权限)
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<el-tree
|
|
||||||
ref="treeRef"
|
|
||||||
:data="routes"
|
|
||||||
show-checkbox
|
|
||||||
node-key="id"
|
|
||||||
:default-checked-keys="initialCheckedKeys"
|
|
||||||
:props="{ label: 'route_title', children: 'children' }"
|
|
||||||
:disabled="!isProvincialAdmin"
|
|
||||||
>
|
|
||||||
<template #default="{ node, data }">
|
|
||||||
<span class="tree-node">
|
|
||||||
<i :class="data.icon" style="margin-right: 8px"></i>
|
|
||||||
<span>{{ data.route_title }}</span>
|
|
||||||
<el-tag
|
|
||||||
v-if="data.enabled"
|
|
||||||
type="success"
|
|
||||||
size="small"
|
|
||||||
style="margin-left: 8px"
|
|
||||||
>
|
|
||||||
已启用
|
|
||||||
</el-tag>
|
|
||||||
<el-tag
|
|
||||||
v-else
|
|
||||||
type="info"
|
|
||||||
size="small"
|
|
||||||
style="margin-left: 8px"
|
|
||||||
>
|
|
||||||
已禁用
|
|
||||||
</el-tag>
|
|
||||||
<span
|
|
||||||
v-if="data.permissions && data.permissions.length > 0"
|
|
||||||
style="margin-left: 8px; color: #909399; font-size: 12px"
|
|
||||||
>
|
|
||||||
({{ data.permissions.length }}个权限)
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-tree>
|
|
||||||
|
|
||||||
<div style="margin-top: 20px" v-if="isProvincialAdmin">
|
|
||||||
<el-button type="primary" @click="savePermissions">
|
|
||||||
保存权限
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="loadRoutes">
|
|
||||||
刷新
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, computed } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
roleId: { type: Number, required: true },
|
|
||||||
roleName: { type: String, required: true }
|
|
||||||
})
|
|
||||||
|
|
||||||
// 检查当前用户是否是省级管理员
|
|
||||||
const currentUserRole = ref('')
|
|
||||||
const isProvincialAdmin = computed(() => currentUserRole.value === 'provincial_admin')
|
|
||||||
|
|
||||||
const treeRef = ref()
|
|
||||||
const routes = ref([])
|
|
||||||
const initialCheckedKeys = ref([])
|
|
||||||
|
|
||||||
// 从Token解析用户角色
|
|
||||||
const parseUserRole = () => {
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
if (token) {
|
|
||||||
const payload = JSON.parse(atob(token.split('.')[1]))
|
|
||||||
currentUserRole.value = payload.user_role || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载路由列表
|
|
||||||
const loadRoutes = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/rbac/roles/${props.roleId}/routes`, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
if (result.code === 200) {
|
|
||||||
routes.value = result.data.routes
|
|
||||||
|
|
||||||
// 提取启用的路由ID
|
|
||||||
initialCheckedKeys.value = []
|
|
||||||
const extractEnabled = (routeList) => {
|
|
||||||
routeList.forEach(route => {
|
|
||||||
if (route.enabled) {
|
|
||||||
initialCheckedKeys.value.push(route.id)
|
|
||||||
}
|
|
||||||
if (route.children) {
|
|
||||||
extractEnabled(route.children)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
extractEnabled(result.data.routes)
|
|
||||||
|
|
||||||
// 等待DOM更新后设置勾选
|
|
||||||
await nextTick()
|
|
||||||
treeRef.value?.setCheckedKeys(initialCheckedKeys.value)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('加载路由失败:' + error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存权限
|
|
||||||
const savePermissions = async () => {
|
|
||||||
if (!isProvincialAdmin.value) {
|
|
||||||
ElMessage.warning('权限不足:仅省级管理员可以修改路由权限')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkedKeys = treeRef.value.getCheckedKeys()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/rbac/roles/${props.roleId}/routes`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
route_ids: checkedKeys,
|
|
||||||
permission: 'RW'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
if (result.code === 200) {
|
|
||||||
ElMessage.success(
|
|
||||||
`权限保存成功!启用 ${result.data.enabled_count} 个路由,` +
|
|
||||||
`禁用 ${result.data.disabled_count} 个路由`
|
|
||||||
)
|
|
||||||
await loadRoutes() // 刷新以更新enabled状态
|
|
||||||
} else if (result.code === 4003) {
|
|
||||||
ElMessage.error('权限不足:仅省级管理员可以修改路由权限')
|
|
||||||
} else {
|
|
||||||
ElMessage.error(result.msg || '保存失败')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error('保存失败:' + error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
parseUserRole()
|
|
||||||
loadRoutes()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tree-node {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4️⃣ 数据库表结构
|
|
||||||
|
|
||||||
### role_route 表(v3.2更新)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE role_route (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
|
||||||
route_id INTEGER NOT NULL REFERENCES sys_routes(id) ON DELETE CASCADE,
|
|
||||||
permission VARCHAR(10) DEFAULT 'RW',
|
|
||||||
status SMALLINT DEFAULT 1 NOT NULL, -- ⭐新增:0=禁用,1=启用
|
|
||||||
created_at TIMESTAMP(6) WITH TIME ZONE DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP(6) WITH TIME ZONE DEFAULT NOW(),
|
|
||||||
|
|
||||||
CONSTRAINT role_route_role_id_route_id_key UNIQUE (role_id, route_id),
|
|
||||||
CONSTRAINT chk_role_route_status CHECK (status IN (0, 1))
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 索引
|
|
||||||
CREATE INDEX idx_role_route_status ON role_route(status);
|
|
||||||
CREATE INDEX idx_role_route_role_status ON role_route(role_id, status);
|
|
||||||
|
|
||||||
-- 注释
|
|
||||||
COMMENT ON COLUMN role_route.status IS '状态:0=禁用,1=启用';
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5️⃣ 核心逻辑说明
|
|
||||||
|
|
||||||
### 状态管理逻辑
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 批量更新时的逻辑:
|
|
||||||
# 1. 先将所有路由设置为禁用(status=0)
|
|
||||||
UPDATE role_route SET status = 0 WHERE role_id = ?
|
|
||||||
|
|
||||||
# 2. 将勾选的路由设置为启用(status=1)
|
|
||||||
UPDATE role_route SET status = 1 WHERE role_id = ? AND route_id IN (?)
|
|
||||||
|
|
||||||
# 3. 如果记录不存在,插入新记录(status默认为1)
|
|
||||||
INSERT INTO role_route (role_id, route_id, permission, status)
|
|
||||||
VALUES (?, ?, 'RW', 1)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 查询逻辑
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 查询时返回所有路由(包括禁用的)
|
|
||||||
SELECT
|
|
||||||
sr.*,
|
|
||||||
rr.status as route_status
|
|
||||||
FROM role_route rr
|
|
||||||
JOIN sys_routes sr ON rr.route_id = sr.id
|
|
||||||
WHERE rr.role_id = ? -- 不限制status,返回所有记录
|
|
||||||
|
|
||||||
# 将 status 字段映射为 enabled
|
|
||||||
route['enabled'] = route['route_status'] == 1
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 版本对比
|
|
||||||
|
|
||||||
| 特性 | v3.1 | v3.2 |
|
|
||||||
|------|------|------|
|
|
||||||
| 取消勾选行为 | 删除记录 | 设置 status=0 |
|
|
||||||
| 路由显示 | 取消勾选后消失 | 取消勾选后仍显示 |
|
|
||||||
| 数据库操作 | DELETE | UPDATE status |
|
|
||||||
| 权限检查 | 无 | 仅省级管理员 |
|
|
||||||
| 返回字段 | assigned_count, removed_count | enabled_count, disabled_count, inserted_count |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 常见问题
|
|
||||||
|
|
||||||
### Q1: 为什么取消勾选后路由还显示?
|
|
||||||
**A**: v3.2 采用状态管理模式,取消勾选只是将 status 设置为 0(禁用),不会删除记录。这样可以保留历史配置,方便恢复。
|
|
||||||
|
|
||||||
### Q2: 非省级管理员调用修改接口会怎样?
|
|
||||||
**A**: 返回 403 错误,提示"权限不足:仅省级管理员可以修改角色路由权限"
|
|
||||||
|
|
||||||
### Q3: enabled 和 assigned 有什么区别?
|
|
||||||
**A**:
|
|
||||||
- `enabled` (v3.2): 基于 role_route.status 字段,表示路由是否启用
|
|
||||||
- `assigned` (v3.2旧版): 基于记录是否存在,已废弃
|
|
||||||
|
|
||||||
### Q4: 如何查看只启用的路由?
|
|
||||||
**A**: 调用接口时传参 `include_disabled=false`(默认为 true,返回所有)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**文档版本**: v3.2
|
|
||||||
**最后更新**: 2025-11-28
|
|
||||||
**维护者**: Backend Team
|
|
||||||
@@ -89,7 +89,7 @@ Authorization: Bearer <token>
|
|||||||
"id": 41,
|
"id": 41,
|
||||||
"route_path": "/rules",
|
"route_path": "/rules",
|
||||||
"route_name": "Rules",
|
"route_name": "Rules",
|
||||||
"route_title": "评查规则库",
|
"route_title": "规则管理",
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"icon": "ri-book-3-line",
|
"icon": "ri-book-3-line",
|
||||||
"sort_order": 3,
|
"sort_order": 3,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+172
-23
@@ -1,6 +1,6 @@
|
|||||||
# 入口模块管理 API 完整技术文档
|
# 入口模块管理 API 完整技术文档
|
||||||
|
|
||||||
> **供前端对接使用** | 更新时间:2025-11-28 | 版本:v2.0
|
> **供前端对接使用** | 更新时间:2025-01-29 | 版本:v2.1
|
||||||
|
|
||||||
## 📋 目录
|
## 📋 目录
|
||||||
|
|
||||||
@@ -390,7 +390,9 @@ LIMIT 20 OFFSET 0;
|
|||||||
**错误响应(500)**
|
**错误响应(500)**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "查询入口模块列表失败: connection to server failed"
|
"code": 500,
|
||||||
|
"msg": "查询入口模块列表失败: connection to server failed",
|
||||||
|
"data": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -470,7 +472,9 @@ WHERE id = 1; -- $1 参数化查询
|
|||||||
**错误响应(404)**
|
**错误响应(404)**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "入口模块不存在: id=999"
|
"code": 404,
|
||||||
|
"msg": "入口模块不存在: id=999",
|
||||||
|
"data": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -606,7 +610,9 @@ RETURNING id, name, description, path, areas, created_at, updated_at;
|
|||||||
**错误响应(400)- 名称重复**
|
**错误响应(400)- 名称重复**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "模块名称已存在: name=合同管理"
|
"code": 400,
|
||||||
|
"msg": "模块名称已存在: name=合同管理",
|
||||||
|
"data": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -756,21 +762,27 @@ RETURNING id, name, description, path, areas, created_at, updated_at;
|
|||||||
**错误响应(404)- 模块不存在**
|
**错误响应(404)- 模块不存在**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "入口模块不存在: id=999"
|
"code": 404,
|
||||||
|
"msg": "入口模块不存在: id=999",
|
||||||
|
"data": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**错误响应(400)- 名称已存在**
|
**错误响应(400)- 名称已存在**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "模块名称已存在: name=案卷智能评查"
|
"code": 400,
|
||||||
|
"msg": "模块名称已存在: name=案卷智能评查",
|
||||||
|
"data": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**错误响应(400)- 没有更新字段**
|
**错误响应(400)- 没有更新字段**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "没有要更新的字段"
|
"code": 400,
|
||||||
|
"msg": "没有要更新的字段",
|
||||||
|
"data": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -849,7 +861,9 @@ DELETE FROM entry_modules WHERE id = 1;
|
|||||||
**错误响应(404)**
|
**错误响应(404)**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "入口模块不存在: id=999"
|
"code": 404,
|
||||||
|
"msg": "入口模块不存在: id=999",
|
||||||
|
"data": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -862,6 +876,73 @@ DELETE FROM entry_modules WHERE id = 1;
|
|||||||
|
|
||||||
### 6. 上传入口模块图标
|
### 6. 上传入口模块图标
|
||||||
|
|
||||||
|
> ⚠️ **重要更新 (v2.1)**:图片上传功能已修复JWT拦截问题,静态资源现在可以无需Token访问。
|
||||||
|
|
||||||
|
#### 🔥 前端常见问题及解决方案
|
||||||
|
|
||||||
|
**问题1:上传时报错 "Field required - body.file"**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 3000,
|
||||||
|
"msg": "参数校验错误",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"type": "missing",
|
||||||
|
"loc": ["body", "file"],
|
||||||
|
"msg": "Field required"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因**:字段名错误或没有使用FormData上传
|
||||||
|
|
||||||
|
**正确做法**:
|
||||||
|
```javascript
|
||||||
|
// ✅ 正确
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', imageFile); // 字段名必须是 "file"
|
||||||
|
|
||||||
|
await fetch(`/api/v3/entry-modules/${moduleId}/image`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
// ✅ 不要设置 Content-Type,让浏览器自动添加boundary
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
// ❌ 错误示例
|
||||||
|
formData.append('image', imageFile); // 字段名错误
|
||||||
|
formData.append('picture', imageFile); // 字段名错误
|
||||||
|
body: JSON.stringify({file: base64}); // 不能用JSON
|
||||||
|
headers: {'Content-Type': 'multipart/form-data'} // 不要手动设置
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题2:图片显示401 Unauthorized**
|
||||||
|
|
||||||
|
这是因为旧版本JWT中间件拦截了MinIO静态资源。
|
||||||
|
|
||||||
|
**已修复** ✅:
|
||||||
|
- MinIO路径(`/docauditai/`)已加入JWT白名单
|
||||||
|
- 图片现在可以**无需Token**直接访问
|
||||||
|
- 测试:`curl http://your-server/docauditai/documents/mz/static/img/entry_module_1.png`
|
||||||
|
|
||||||
|
**问题3:错误的图片访问路径**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ 错误 - 硬编码路径
|
||||||
|
const imageUrl = `/docauditai/entryModule/${module.code}`;
|
||||||
|
|
||||||
|
// ✅ 正确 - 使用数据库path字段
|
||||||
|
const imageUrl = `http://172.16.0.81:9000/docauditai/${module.path}`;
|
||||||
|
|
||||||
|
// ✅ 或者使用上传接口返回的url字段(推荐)
|
||||||
|
const result = await uploadImage(moduleId, file);
|
||||||
|
const imageUrl = result.data.url;
|
||||||
|
```
|
||||||
|
|
||||||
#### 接口定义
|
#### 接口定义
|
||||||
|
|
||||||
```http
|
```http
|
||||||
@@ -1067,28 +1148,52 @@ WHERE id = 1;
|
|||||||
**错误响应(400)- 不支持的格式**
|
**错误响应(400)- 不支持的格式**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "不支持的图片格式: .pdf,支持的格式: .jpg, .jpeg, .png, .gif, .webp, .svg"
|
"code": 400,
|
||||||
|
"msg": "不支持的图片格式: .pdf,支持的格式: .jpg, .jpeg, .png, .gif, .webp, .svg",
|
||||||
|
"data": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**错误响应(400)- 文件为空**
|
**错误响应(400)- 文件为空**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "文件内容为空"
|
"code": 400,
|
||||||
|
"msg": "文件内容为空",
|
||||||
|
"data": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**错误响应(404)- 模块不存在**
|
**错误响应(404)- 模块不存在**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "入口模块不存在: id=999"
|
"code": 404,
|
||||||
|
"msg": "入口模块不存在: id=999",
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应(422)- 缺少file字段** ⚠️ **常见错误**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 3000,
|
||||||
|
"msg": "参数校验错误",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"type": "missing",
|
||||||
|
"loc": ["body", "file"],
|
||||||
|
"msg": "Field required",
|
||||||
|
"input": null
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**错误响应(500)- MinIO上传失败**
|
**错误响应(500)- MinIO上传失败**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"detail": "上传入口模块图片失败: S3 error: Access Denied"
|
"code": 500,
|
||||||
|
"msg": "上传入口模块图片失败: S3 error: Access Denied",
|
||||||
|
"data": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1465,22 +1570,30 @@ api.interceptors.response.use(
|
|||||||
(error) => {
|
(error) => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
const { status, data } = error.response;
|
const { status, data } = error.response;
|
||||||
|
// ⭐ v2.1更新:错误响应统一格式 {code, msg, data}
|
||||||
|
const errorMsg = data.msg || data.detail || '请求失败';
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 401:
|
case 401:
|
||||||
// Token过期,跳转登录
|
// Token过期,跳转登录
|
||||||
|
console.error('未授权:', errorMsg);
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
break;
|
break;
|
||||||
case 403:
|
case 403:
|
||||||
// 无权限
|
// 无权限
|
||||||
console.error('权限不足');
|
console.error('权限不足:', errorMsg);
|
||||||
break;
|
break;
|
||||||
case 404:
|
case 404:
|
||||||
// 资源不存在
|
// 资源不存在
|
||||||
console.error(data.detail || '资源不存在');
|
console.error('资源不存在:', errorMsg);
|
||||||
|
break;
|
||||||
|
case 422:
|
||||||
|
// 参数校验错误
|
||||||
|
console.error('参数错误:', errorMsg, data.data);
|
||||||
break;
|
break;
|
||||||
case 500:
|
case 500:
|
||||||
// 服务器错误
|
// 服务器错误
|
||||||
console.error(data.detail || '服务器错误');
|
console.error('服务器错误:', errorMsg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1554,20 +1667,20 @@ export const entryModuleApi = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传图标
|
* 上传图标
|
||||||
|
* ⚠️ 注意:字段名必须是 "file",不要手动设置Content-Type
|
||||||
*/
|
*/
|
||||||
uploadImage: (id: number, file: File) => {
|
uploadImage: (id: number, file: File) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file); // ✅ 字段名必须是 "file"
|
||||||
|
|
||||||
return api.post<{
|
return api.post<{
|
||||||
code: number;
|
code: number;
|
||||||
msg: string;
|
msg: string;
|
||||||
data: ImageUploadResponse;
|
data: ImageUploadResponse;
|
||||||
}>(`/entry-modules/${id}/image`, formData, {
|
}>(`/entry-modules/${id}/image`, formData
|
||||||
headers: {
|
// ✅ 不要设置Content-Type,axios会自动设置
|
||||||
'Content-Type': 'multipart/form-data'
|
// 如果手动设置会缺少boundary参数导致上传失败
|
||||||
}
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
@@ -1776,7 +1889,43 @@ WHERE module = 'entry_module';
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**文档版本**:v2.0
|
## 📝 版本更新记录
|
||||||
**最后更新**:2025-11-28
|
|
||||||
|
### v2.1 (2025-01-29)
|
||||||
|
|
||||||
|
**重要更新**:
|
||||||
|
1. ✅ 修复JWT拦截MinIO静态资源问题
|
||||||
|
- MinIO路径(`/docauditai/`)已加入JWT白名单
|
||||||
|
- 图片现在可以无需Token访问
|
||||||
|
|
||||||
|
2. ✅ 统一错误响应格式
|
||||||
|
- 旧格式:`{"detail": "错误信息"}`
|
||||||
|
- 新格式:`{"code": xxx, "msg": "错误信息", "data": []}`
|
||||||
|
|
||||||
|
3. ✅ 新增图片上传常见问题解决方案
|
||||||
|
- 详细说明了"Field required - body.file"错误的解决方法
|
||||||
|
- 提供了正确的FormData使用示例
|
||||||
|
- 强调字段名必须是 "file"
|
||||||
|
|
||||||
|
4. ✅ 创建测试工具
|
||||||
|
- 新增HTML测试工具:`scripts/test_entry_module_image_upload.html`
|
||||||
|
- 可视化测试界面,帮助调试图片上传功能
|
||||||
|
|
||||||
|
**前端重要修改**:
|
||||||
|
- ⚠️ 删除手动设置 `Content-Type: multipart/form-data`
|
||||||
|
- ⚠️ 使用 `data.msg` 替代 `data.detail` 获取错误信息
|
||||||
|
- ⚠️ 图片访问不要硬编码路径,使用数据库path字段
|
||||||
|
|
||||||
|
### v2.0 (2025-11-28)
|
||||||
|
|
||||||
|
- 完整的API文档
|
||||||
|
- 数据库设计详解
|
||||||
|
- 前端对接指南
|
||||||
|
- TypeScript类型定义
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**当前版本**:v2.1
|
||||||
|
**最后更新**:2025-01-29
|
||||||
**维护者**:Backend Team
|
**维护者**:Backend Team
|
||||||
**反馈渠道**:[提交Issue](http://git.7bm.co:1024/leke/docauditai/-/issues)
|
**反馈渠道**:[提交Issue](http://git.7bm.co:1024/leke/docauditai/-/issues)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
|||||||
|
# 前端工程文档导航
|
||||||
|
|
||||||
|
> 最后整理:2026-05-04
|
||||||
|
> 说明:本目录主要存放 `new_doc_review` 前端实现、联调、部署、能力评估类文档。
|
||||||
|
|
||||||
|
## 当前优先阅读
|
||||||
|
|
||||||
|
- 权限子页映射规范:`new_doc_review/docs/route-alias-guidelines.md`
|
||||||
|
- PostgREST 迁移盘点:
|
||||||
|
- `new_doc_review/docs/PostgREST使用情况-后端API替代建议.md`
|
||||||
|
- `new_doc_review/docs/PostgREST请求模块清单.md`
|
||||||
|
- `new_doc_review/docs/PostgREST实际使用清单.md`
|
||||||
|
- 文档类型页面改造背景:`new_doc_review/docs/文档类型管理CRUD详细分析.md`
|
||||||
|
- 评查点域联调资料:`new_doc_review/docs/evaluation/`
|
||||||
|
- 部署与运行:`new_doc_review/docs/deployment-config.md`、`new_doc_review/docs/docker-deployment.md`
|
||||||
|
|
||||||
|
## 按主题分类
|
||||||
|
|
||||||
|
| 主题 | 重点文档 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 权限 / 会话 / 登录 | `RBAC路由权限集成说明.md`、`RBAC路由权限集成_修复说明.md`、`JWT_IMPLEMENTATION.md`、`统一登录流程_实施记录.md` | 登录、JWT、路由权限接入 |
|
||||||
|
| PostgREST 迁移 | `PostgREST使用情况-后端API替代建议.md`、`PostgREST请求模块清单.md`、`PostgREST实际使用清单.md`、`PostgREST未使用函数清单.md` | 哪些页面仍依赖 PostgREST、替代优先级 |
|
||||||
|
| 首页 / 数据首页 | `getHomeData_完整逻辑文档.md`、`home-api-analysis.md` | 首页数据获取与结构说明 |
|
||||||
|
| 文档类型 / 规则页 | `文档类型管理CRUD详细分析.md`、`attribute_type_frontend_integration.md`、`evaluation/` | 文档类型、评查点、属性类型联调 |
|
||||||
|
| 合同模板 / 起草 | `contract-drafting-*.md`、`contract-draft-*.md` | 合同模板搜索、列表、起草与历史重构记录 |
|
||||||
|
| Dify / 智能助手 | `dify-proxy-backend-integration.md`、`new-dify/`、`Token管理架构优化总结.md` | 智能助手、代理接入、Token 管理 |
|
||||||
|
| 文件 / 文档能力 | `minio-docx-extraction-fix.md`、`docxtemplater-placeholder-extraction.md`、`react-pdf功能总结.md` | 文件处理、占位符提取、PDF 能力 |
|
||||||
|
| 部署 / 运行维护 | `deployment-config.md`、`deployment-checklist.md`、`docker-deployment.md`、`postgresql-backup-guide.md` | 环境部署与备份 |
|
||||||
|
|
||||||
|
## 使用约束
|
||||||
|
|
||||||
|
- 这里偏前端实现细节,不替代 `docs/` 下的项目主文档
|
||||||
|
- 涉及当前主链路的事实判断,优先对照 `docs/HANDOFF.md` 与 `docs/接口/README.md`
|
||||||
|
- 临时排障文档若已失效,应合并进正式文档后删除,不再继续堆积“紧急说明”类文件
|
||||||
@@ -1,398 +0,0 @@
|
|||||||
# 🚨 紧急:前端Dify集成问题修复指南
|
|
||||||
|
|
||||||
## 问题现状
|
|
||||||
|
|
||||||
**当前错误**:
|
|
||||||
```javascript
|
|
||||||
// ❌ 错误的Authorization头
|
|
||||||
Authorization: Bearer app-lHn5EmeACIaLjG9yz0rYIFfM // 这是Dify API Key,不是JWT!
|
|
||||||
|
|
||||||
// ❌ 错误的user字段
|
|
||||||
user: "user_34a1d450-6a24-4db7-b1e7-8dd7c0876f14:sess_3z84y7dtuhr" // UUID格式
|
|
||||||
```
|
|
||||||
|
|
||||||
**导致的问题**:
|
|
||||||
1. 无法加载历史对话记录(user不匹配)
|
|
||||||
2. 对话没有按用户隔离(所有人共享对话)
|
|
||||||
3. 安全问题(API Key暴露在前端)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 必须修改的3个地方
|
|
||||||
|
|
||||||
### 修改1: Authorization头 - 使用JWT替代Dify API Key
|
|
||||||
|
|
||||||
#### ❌ 旧代码(错误)
|
|
||||||
```typescript
|
|
||||||
// app/services/dify-client.server.ts 或类似文件
|
|
||||||
const DIFY_API_KEY = "app-lHn5EmeACIaLjG9yz0rYIFfM";
|
|
||||||
|
|
||||||
async function callDifyAPI(endpoint: string, data: any) {
|
|
||||||
const response = await fetch(`${DIFY_BASE_URL}${endpoint}`, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${DIFY_API_KEY}`, // ❌ 错误!暴露API Key
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ✅ 新代码(正确)
|
|
||||||
```typescript
|
|
||||||
// app/services/dify-client.server.ts
|
|
||||||
// 1. 移除DIFY_API_KEY配置
|
|
||||||
// const DIFY_API_KEY = "..."; // 删除这行
|
|
||||||
|
|
||||||
// 2. 从用户session获取JWT
|
|
||||||
import { getUserSession } from "~/api/login/auth.server";
|
|
||||||
|
|
||||||
async function callDifyAPI(request: Request, endpoint: string, data: any) {
|
|
||||||
// 获取JWT
|
|
||||||
const { frontendJWT } = await getUserSession(request);
|
|
||||||
|
|
||||||
if (!frontendJWT) {
|
|
||||||
throw new Error("未登录,请先登录");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用JWT作为Authorization头
|
|
||||||
const response = await fetch(`http://172.16.0.55:8000/dify${endpoint}`, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${frontendJWT}`, // ✅ 正确!使用JWT
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 修改2: 请求URL - 指向FastAPI代理而非直连Dify
|
|
||||||
|
|
||||||
#### ❌ 旧代码(错误)
|
|
||||||
```typescript
|
|
||||||
// 直连Dify API
|
|
||||||
const DIFY_BASE_URL = "http://nas.7bm.co:12980/v1";
|
|
||||||
|
|
||||||
// 示例:获取会话列表
|
|
||||||
const response = await fetch(
|
|
||||||
`${DIFY_BASE_URL}/conversations?user=${user}&limit=100`,
|
|
||||||
// ...
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ✅ 新代码(正确)
|
|
||||||
```typescript
|
|
||||||
// 通过FastAPI代理访问Dify
|
|
||||||
const API_BASE_URL = "http://172.16.0.55:8000"; // FastAPI地址
|
|
||||||
|
|
||||||
// 示例:获取会话列表
|
|
||||||
const response = await fetch(
|
|
||||||
`${API_BASE_URL}/dify/conversations?limit=100`, // ✅ 走/dify代理,不传user
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${frontendJWT}` // ✅ 使用JWT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 修改3: user字段 - 完全移除,让后端自动添加
|
|
||||||
|
|
||||||
#### ❌ 旧代码(错误)
|
|
||||||
```typescript
|
|
||||||
// 生成UUID格式的user
|
|
||||||
function generateUserId() {
|
|
||||||
return `user_${crypto.randomUUID()}:sess_${randomString()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送消息
|
|
||||||
const data = {
|
|
||||||
query: "你好",
|
|
||||||
user: generateUserId(), // ❌ 错误!UUID格式
|
|
||||||
conversation_id: null,
|
|
||||||
response_mode: "streaming",
|
|
||||||
inputs: {}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ✅ 新代码(正确)
|
|
||||||
```typescript
|
|
||||||
// 完全不传user字段,后端会自动添加JWT的username
|
|
||||||
const data = {
|
|
||||||
query: "你好",
|
|
||||||
// user字段完全不传! ✅ 正确
|
|
||||||
conversation_id: null,
|
|
||||||
response_mode: "streaming",
|
|
||||||
inputs: {}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 完整的修改示例
|
|
||||||
|
|
||||||
### 示例1: 获取会话列表
|
|
||||||
|
|
||||||
#### ❌ 旧代码
|
|
||||||
```typescript
|
|
||||||
// app/routes/api.conversations.tsx
|
|
||||||
export async function loader({ request }: LoaderFunctionArgs) {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
const user = url.searchParams.get("user") || generateUserId();
|
|
||||||
const limit = url.searchParams.get("limit") || "100";
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`http://nas.7bm.co:12980/v1/conversations?user=${user}&limit=${limit}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer app-lHn5EmeACIaLjG9yz0rYIFfM`, // ❌ Dify API Key
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return json(await response.json());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ✅ 新代码
|
|
||||||
```typescript
|
|
||||||
// app/routes/api.conversations.tsx
|
|
||||||
import { getUserSession } from "~/api/login/auth.server";
|
|
||||||
|
|
||||||
export async function loader({ request }: LoaderFunctionArgs) {
|
|
||||||
// 1. 获取JWT
|
|
||||||
const { frontendJWT } = await getUserSession(request);
|
|
||||||
|
|
||||||
if (!frontendJWT) {
|
|
||||||
return json({ error: "未登录" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 调用FastAPI代理(不传user,后端自动添加)
|
|
||||||
const url = new URL(request.url);
|
|
||||||
const limit = url.searchParams.get("limit") || "100";
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`http://172.16.0.55:8000/dify/conversations?limit=${limit}`, // ✅ 走代理
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${frontendJWT}`, // ✅ 使用JWT
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return json(await response.json());
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 示例2: 发送消息
|
|
||||||
|
|
||||||
#### ❌ 旧代码
|
|
||||||
```typescript
|
|
||||||
// app/routes/api.chat-messages.tsx
|
|
||||||
export async function action({ request }: ActionFunctionArgs) {
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`http://nas.7bm.co:12980/v1/chat-messages`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer app-lHn5EmeACIaLjG9yz0rYIFfM`, // ❌ Dify API Key
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
query: body.query,
|
|
||||||
user: body.user || generateUserId(), // ❌ UUID格式
|
|
||||||
conversation_id: body.conversation_id,
|
|
||||||
response_mode: "streaming",
|
|
||||||
inputs: body.inputs || {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Response(response.body, {
|
|
||||||
headers: { 'Content-Type': 'text/event-stream' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ✅ 新代码
|
|
||||||
```typescript
|
|
||||||
// app/routes/api.chat-messages.tsx
|
|
||||||
import { getUserSession } from "~/api/login/auth.server";
|
|
||||||
|
|
||||||
export async function action({ request }: ActionFunctionArgs) {
|
|
||||||
// 1. 获取JWT
|
|
||||||
const { frontendJWT } = await getUserSession(request);
|
|
||||||
|
|
||||||
if (!frontendJWT) {
|
|
||||||
return json({ error: "未登录" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 解析请求体
|
|
||||||
const body = await request.json();
|
|
||||||
|
|
||||||
// 3. 调用FastAPI代理(不传user,后端自动添加)
|
|
||||||
const response = await fetch(
|
|
||||||
`http://172.16.0.55:8000/dify/chat-messages`, // ✅ 走代理
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${frontendJWT}`, // ✅ 使用JWT
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
query: body.query,
|
|
||||||
// user字段完全不传 ✅ 后端自动添加
|
|
||||||
conversation_id: body.conversation_id,
|
|
||||||
response_mode: "streaming",
|
|
||||||
inputs: body.inputs || {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Response(response.body, {
|
|
||||||
headers: { 'Content-Type': 'text/event-stream' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 检查清单
|
|
||||||
|
|
||||||
### 前端开发者必须检查的文件
|
|
||||||
|
|
||||||
1. **Dify客户端配置文件**
|
|
||||||
- 位置:`app/services/dify-client.server.ts` 或类似
|
|
||||||
- 检查:是否还有`DIFY_API_KEY`定义?(应该删除)
|
|
||||||
- 检查:是否还有`DIFY_BASE_URL = "http://nas.7bm.co:12980"`?(应改为FastAPI地址)
|
|
||||||
|
|
||||||
2. **API路由文件**
|
|
||||||
- 位置:`app/routes/api.*.tsx` 中所有与Dify相关的
|
|
||||||
- 检查:所有`fetch`调用是否指向`http://172.16.0.55:8000/dify/*`?
|
|
||||||
- 检查:所有`Authorization`头是否使用`frontendJWT`?
|
|
||||||
|
|
||||||
3. **user字段生成逻辑**
|
|
||||||
- 搜索关键字:`generateUserId`, `user_`, `crypto.randomUUID`
|
|
||||||
- 检查:是否还在生成UUID格式的user?(应该完全移除)
|
|
||||||
|
|
||||||
4. **环境变量配置**
|
|
||||||
- 位置:`.env` 或 `.env.local`
|
|
||||||
- 检查:是否还有`DIFY_API_KEY`或`NEXT_PUBLIC_APP_KEY`?(应该删除)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 验证方法
|
|
||||||
|
|
||||||
修改完成后,使用浏览器开发者工具验证:
|
|
||||||
|
|
||||||
### 1. 检查Authorization头
|
|
||||||
打开 Network 标签,找到Dify相关请求:
|
|
||||||
|
|
||||||
**✅ 正确的请求头**:
|
|
||||||
```
|
|
||||||
Request URL: http://172.16.0.55:8000/dify/conversations?limit=100
|
|
||||||
Request Headers:
|
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... (JWT格式,很长)
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
|
|
||||||
**❌ 错误的请求头**:
|
|
||||||
```
|
|
||||||
Request URL: http://nas.7bm.co:12980/v1/conversations?user=xxx
|
|
||||||
Request Headers:
|
|
||||||
Authorization: Bearer app-lHn5EmeACIaLjG9yz0rYIFfM (Dify API Key,很短)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 检查请求体
|
|
||||||
对于POST请求(如发送消息):
|
|
||||||
|
|
||||||
**✅ 正确的请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"query": "你好",
|
|
||||||
"conversation_id": null,
|
|
||||||
"response_mode": "streaming",
|
|
||||||
"inputs": {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
注意:**没有user字段**
|
|
||||||
|
|
||||||
**❌ 错误的请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"query": "你好",
|
|
||||||
"user": "user_34a1d450-6a24-4db7-b1e7-8dd7c0876f14:sess_xxx",
|
|
||||||
"conversation_id": null,
|
|
||||||
"response_mode": "streaming",
|
|
||||||
"inputs": {}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 检查后端日志
|
|
||||||
修改后,后端日志应该显示:
|
|
||||||
|
|
||||||
```log
|
|
||||||
[INFO] Dify请求用户: admin (ID: 5, 路径: conversations)
|
|
||||||
[INFO] 自动添加user参数: admin
|
|
||||||
[INFO] 查询参数: {'user': 'admin', 'limit': '100'}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 检查Dify后台
|
|
||||||
在Dify管理界面的对话列表中,应该看到:
|
|
||||||
- 新对话的用户显示为:`admin`(或实际的username)
|
|
||||||
- 不再是:`user_34a1d450...`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 常见错误
|
|
||||||
|
|
||||||
### 错误1: 前端仍使用DIFY_API_KEY
|
|
||||||
**症状**:Authorization头是`Bearer app-xxx`(短字符串)
|
|
||||||
**解决**:删除所有`DIFY_API_KEY`定义,改用`frontendJWT`
|
|
||||||
|
|
||||||
### 错误2: 前端直连Dify
|
|
||||||
**症状**:请求URL是`http://nas.7bm.co:12980/v1/...`
|
|
||||||
**解决**:所有URL改为`http://172.16.0.55:8000/dify/...`
|
|
||||||
|
|
||||||
### 错误3: 仍在传递UUID格式的user
|
|
||||||
**症状**:请求体包含`"user": "user_34a1d450..."`
|
|
||||||
**解决**:完全删除user字段,不要传递
|
|
||||||
|
|
||||||
### 错误4: JWT获取失败
|
|
||||||
**症状**:`frontendJWT`为`null`或`undefined`
|
|
||||||
**解决**:检查`getUserSession`导入路径和session配置
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 修改总结
|
|
||||||
|
|
||||||
| 项目 | 旧值 | 新值 |
|
|
||||||
|------|------|------|
|
|
||||||
| Authorization头 | `Bearer app-lHn5EmeACIaLjG9yz0rYIFfM` | `Bearer {frontendJWT}` |
|
|
||||||
| 请求URL | `http://nas.7bm.co:12980/v1/*` | `http://172.16.0.55:8000/dify/*` |
|
|
||||||
| user字段 | `user_34a1d450-...` | 完全不传(后端自动添加) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 期望结果
|
|
||||||
|
|
||||||
修改完成后:
|
|
||||||
- ✅ 每个用户只能看到自己的对话记录
|
|
||||||
- ✅ Dify后台显示真实的username(如admin)
|
|
||||||
- ✅ API Key不再暴露在前端
|
|
||||||
- ✅ 所有对话按用户隔离
|
|
||||||
- ✅ 历史对话记录正常加载
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**紧急程度**: 🔴 高优先级
|
|
||||||
**预计修改时间**: 30-60分钟
|
|
||||||
**影响范围**: 所有Dify相关功能
|
|
||||||
|
|
||||||
**联系后端**: 如有疑问,请查看`docs/dify-frontend-user-field-guide.md`获取更多细节
|
|
||||||
@@ -558,7 +558,7 @@ const wrongUsers = await getRoleUsers(3); // 数据库中不存在
|
|||||||
"id": 41,
|
"id": 41,
|
||||||
"route_path": "/rules",
|
"route_path": "/rules",
|
||||||
"route_name": "Rules",
|
"route_name": "Rules",
|
||||||
"route_title": "评查规则库",
|
"route_title": "规则管理",
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"icon": "ri-book-3-line",
|
"icon": "ri-book-3-line",
|
||||||
"sort_order": 3,
|
"sort_order": 3,
|
||||||
@@ -833,7 +833,7 @@ sys_routes 表:仅存储页面路由(侧边栏菜单)
|
|||||||
permissions 表:存储API操作权限(通过 route_id 关联到页面)
|
permissions 表:存储API操作权限(通过 route_id 关联到页面)
|
||||||
|
|
||||||
页面路由
|
页面路由
|
||||||
├── 评查规则库 (/rules)
|
├── 规则管理 (/rules)
|
||||||
│ ├── [权限] 查看评查点分组列表 (GET /api/v3/evaluation-point-groups)
|
│ ├── [权限] 查看评查点分组列表 (GET /api/v3/evaluation-point-groups)
|
||||||
│ ├── [权限] 创建评查点分组 (POST /api/v3/evaluation-point-groups)
|
│ ├── [权限] 创建评查点分组 (POST /api/v3/evaluation-point-groups)
|
||||||
│ ├── [权限] 查看评查点规则列表 (GET /api/v3/evaluation-points)
|
│ ├── [权限] 查看评查点规则列表 (GET /api/v3/evaluation-points)
|
||||||
@@ -880,7 +880,7 @@ interface Permission {
|
|||||||
│ │
|
│ │
|
||||||
│ ☑ 系统概览 (/home) │
|
│ ☑ 系统概览 (/home) │
|
||||||
│ │
|
│ │
|
||||||
│ ☑ 评查规则库 (/rules) │
|
│ ☑ 规则管理 (/rules) │
|
||||||
│ ├─ ☑ 查看评查点分组列表 [GET] │
|
│ ├─ ☑ 查看评查点分组列表 [GET] │
|
||||||
│ ├─ ☑ 创建评查点分组 [POST] │
|
│ ├─ ☑ 创建评查点分组 [POST] │
|
||||||
│ ├─ ☑ 更新评查点分组 [PUT] │
|
│ ├─ ☑ 更新评查点分组 [PUT] │
|
||||||
|
|||||||
@@ -68,18 +68,21 @@
|
|||||||
- `/config-lists/new` -> `/config-lists`
|
- `/config-lists/new` -> `/config-lists`
|
||||||
- `/prompts/new` -> `/prompts`
|
- `/prompts/new` -> `/prompts`
|
||||||
- `/rule-groups/new` -> `/rule-groups`
|
- `/rule-groups/new` -> `/rule-groups`
|
||||||
- `/rules/new` -> `/rules/list`
|
- `/rules/new` -> `/rules`
|
||||||
|
- `/documents/list` -> `/documents`
|
||||||
- `/documents/edit` -> `/documents`
|
- `/documents/edit` -> `/documents`
|
||||||
|
|
||||||
- 模块内子页
|
- 模块内子页
|
||||||
- `/contract-template/search/results` -> `/contract-template/search`
|
- `/contract-template/search/results` -> `/contract-template/search`
|
||||||
- `/contract-template/detail/:id` -> `/contract-template`
|
- `/contract-template/detail/:id` -> `/contract-template/list`
|
||||||
- `/contract-draft/:id` -> `/contract-template`
|
- `/contract-draft/:id` -> `/contract-template/list`
|
||||||
|
- `/chat-with-llm/chat` -> `/chat-with-llm`
|
||||||
|
- `/chat-with-llm/dataset-manager` -> `/chat-with-llm`
|
||||||
|
|
||||||
- 历史兼容页
|
- 历史兼容页
|
||||||
- `/reviewsTest` -> `/reviews`
|
- `/reviewsTest` -> `/reviews`
|
||||||
- `/rulesTest/list` -> `/rules/list`
|
- `/rulesTest/list` -> `/rules`
|
||||||
- `/rulesTest/detail` -> `/rules/list`
|
- `/rulesTest/detail` -> `/rules`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user