e7646d17a6
## 修复内容
### 1. 修复一级分组过滤问题
- **问题**: getEvaluationPointGroups 函数忽略了 pid 参数,导致返回所有分组(包括二级分组)
- **修复**: 添加 pid 参数处理逻辑,支持传递 "null" 字符串来查询一级分组
- **文件**: app/api/evaluation_points/rule-groups.ts:1186-1198
### 2. 修复 React key 重复警告
- **问题**: 父分组和子分组可能有相同的 ID,导致 "Encountered two children with the same key" 警告
- **修复**: 将 rowKey 从简单的 "id" 改为根据 isParent 生成唯一 key
- **文件**: app/routes/rule-groups._index.tsx:817
### 3. 新增后端 API 规范文档
- **文件**: docs/evaluation/evaluation_point_groups_backend_api_spec.md
- **内容**:
- 完整的 9 个 FastAPI v3 接口规范
- Python Pydantic 模型定义
- TypeScript 接口定义
- pid 参数处理说明(字符串 "null" 转换为 None)
- 10 个完整测试用例
- 数据库表结构建议
## 技术细节
**pid 参数处理**:
```typescript
// 前端发送
GET /api/v3/evaluation-point-groups?pid=null&page=1
// 后端需要识别字符串 "null" 并转换为 None/NULL
if (pid == "null") {
query = query.filter(EvaluationPointGroup.pid.is_(None))
}
```
**唯一 key 生成**:
```typescript
rowKey={(record) => record.isParent ? `parent-${record.id}` : `child-${record.id}`}
```
🔗 相关文档: docs/evaluation/evaluation_point_groups_backend_api_spec.md
38 KiB
38 KiB
评查点分组 FastAPI v3 后端接口规范
📚 目录
数据模型定义
Python (FastAPI/Pydantic) 模型
from pydantic import BaseModel, Field, validator
from typing import Optional, List
from datetime import datetime
from enum import Enum
# ==================== 基础模型 ====================
class EvaluationPointGroupBase(BaseModel):
"""评查点分组基础模型"""
id: int = Field(..., description="分组ID")
pid: Optional[int] = Field(None, description="父分组ID,null表示一级分组")
name: str = Field(..., min_length=1, max_length=100, description="分组名称")
code: str = Field(..., min_length=1, max_length=50, description="分组编码")
description: Optional[str] = Field(None, max_length=500, description="分组描述")
is_enabled: bool = Field(True, description="是否启用")
created_at: datetime = Field(..., description="创建时间")
updated_at: datetime = Field(..., description="更新时间")
rule_count: Optional[int] = Field(0, ge=0, description="关联的评查点数量(包含子分组)")
class Config:
json_schema_extra = {
"example": {
"id": 1,
"pid": None,
"name": "合同管理类",
"code": "CONTRACT_001",
"description": "合同管理相关的评查点分组",
"is_enabled": True,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"rule_count": 25
}
}
class EvaluationPointGroupTree(EvaluationPointGroupBase):
"""评查点分组树形结构模型"""
children: List['EvaluationPointGroupTree'] = Field(default_factory=list, description="子分组列表")
class Config:
json_schema_extra = {
"example": {
"id": 1,
"pid": None,
"name": "合同管理类",
"code": "CONTRACT_001",
"description": "合同管理相关的评查点分组",
"is_enabled": True,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"rule_count": 25,
"children": [
{
"id": 2,
"pid": 1,
"name": "合同审批",
"code": "CONTRACT_001_001",
"description": None,
"is_enabled": True,
"created_at": "2025-01-15T10:31:00Z",
"updated_at": "2025-01-15T10:31:00Z",
"rule_count": 10,
"children": []
}
]
}
}
# 启用递归模型
EvaluationPointGroupTree.model_rebuild()
# ==================== 请求模型 ====================
class EvaluationPointGroupCreateRequest(BaseModel):
"""创建分组请求模型"""
pid: Optional[int] = Field(None, description="父分组ID,null表示创建一级分组")
name: str = Field(..., min_length=1, max_length=100, description="分组名称")
code: str = Field(..., min_length=1, max_length=50, description="分组编码,必须唯一")
description: Optional[str] = Field(None, max_length=500, description="分组描述")
is_enabled: bool = Field(True, description="是否启用")
@validator('name')
def name_must_not_be_empty(cls, v):
if not v or not v.strip():
raise ValueError('分组名称不能为空')
return v.strip()
@validator('code')
def code_must_be_valid(cls, v):
if not v or not v.strip():
raise ValueError('分组编码不能为空')
# 可以添加更多编码格式验证
return v.strip().upper()
class Config:
json_schema_extra = {
"example": {
"pid": 1,
"name": "新分组名称",
"code": "NEW_GROUP_001",
"description": "分组描述",
"is_enabled": True
}
}
class EvaluationPointGroupUpdateRequest(BaseModel):
"""更新分组请求模型"""
pid: Optional[int] = Field(None, description="父分组ID")
name: str = Field(..., min_length=1, max_length=100, description="分组名称")
code: str = Field(..., min_length=1, max_length=50, description="分组编码")
description: Optional[str] = Field(None, max_length=500, description="分组描述")
is_enabled: bool = Field(..., description="是否启用")
@validator('name')
def name_must_not_be_empty(cls, v):
if not v or not v.strip():
raise ValueError('分组名称不能为空')
return v.strip()
class Config:
json_schema_extra = {
"example": {
"pid": 1,
"name": "更新后的名称",
"code": "UPDATED_001",
"description": "更新后的描述",
"is_enabled": False
}
}
class BatchUpdateStatusRequest(BaseModel):
"""批量更新状态请求模型"""
ids: List[int] = Field(..., min_items=1, description="要更新的分组ID列表")
is_enabled: bool = Field(..., description="目标状态")
class Config:
json_schema_extra = {
"example": {
"ids": [1, 2, 3],
"is_enabled": False
}
}
class BatchDeleteRequest(BaseModel):
"""批量删除请求模型"""
ids: List[int] = Field(..., min_items=1, description="要删除的分组ID列表")
class Config:
json_schema_extra = {
"example": {
"ids": [10, 11, 12]
}
}
# ==================== 响应模型 ====================
class EvaluationPointGroupListResponse(BaseModel):
"""分组列表响应模型"""
data: List[EvaluationPointGroupBase] = Field(..., description="分组列表")
total: int = Field(..., ge=0, description="总记录数")
page: int = Field(..., ge=1, description="当前页码")
page_size: int = Field(..., ge=1, le=1000, description="每页记录数")
class Config:
json_schema_extra = {
"example": {
"data": [
{
"id": 1,
"pid": None,
"name": "合同管理类",
"code": "CONTRACT_001",
"description": "合同管理相关的评查点分组",
"is_enabled": True,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"rule_count": 25
}
],
"total": 100,
"page": 1,
"page_size": 20
}
}
class EvaluationPointGroupTreeResponse(BaseModel):
"""树形结构响应模型"""
data: List[EvaluationPointGroupTree] = Field(..., description="树形结构数据")
class Config:
json_schema_extra = {
"example": {
"data": [
{
"id": 1,
"pid": None,
"name": "合同管理类",
"code": "CONTRACT_001",
"description": "合同管理相关的评查点分组",
"is_enabled": True,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"rule_count": 25,
"children": [
{
"id": 2,
"pid": 1,
"name": "合同审批",
"code": "CONTRACT_001_001",
"description": None,
"is_enabled": True,
"created_at": "2025-01-15T10:31:00Z",
"updated_at": "2025-01-15T10:31:00Z",
"rule_count": 10,
"children": []
}
]
}
]
}
}
class EvaluationPointGroupDetailResponse(BaseModel):
"""分组详情响应模型"""
data: EvaluationPointGroupTree = Field(..., description="分组详情")
class Config:
json_schema_extra = {
"example": {
"data": {
"id": 1,
"pid": None,
"name": "合同管理类",
"code": "CONTRACT_001",
"description": "合同管理相关的评查点分组",
"is_enabled": True,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"rule_count": 25,
"children": []
}
}
}
class EvaluationPointGroupCreateResponse(BaseModel):
"""创建分组响应模型"""
data: EvaluationPointGroupBase = Field(..., description="创建的分组信息")
message: str = Field(default="创建成功", description="操作消息")
class Config:
json_schema_extra = {
"example": {
"data": {
"id": 100,
"pid": 1,
"name": "新分组名称",
"code": "NEW_GROUP_001",
"description": "分组描述",
"is_enabled": True,
"created_at": "2025-01-20T14:30:00Z",
"updated_at": "2025-01-20T14:30:00Z",
"rule_count": 0
},
"message": "创建成功"
}
}
class EvaluationPointGroupUpdateResponse(BaseModel):
"""更新分组响应模型"""
data: EvaluationPointGroupBase = Field(..., description="更新后的分组信息")
message: str = Field(default="更新成功", description="操作消息")
class Config:
json_schema_extra = {
"example": {
"data": {
"id": 100,
"pid": 1,
"name": "更新后的名称",
"code": "UPDATED_001",
"description": "更新后的描述",
"is_enabled": False,
"created_at": "2025-01-20T14:30:00Z",
"updated_at": "2025-01-20T14:35:00Z",
"rule_count": 5
},
"message": "更新成功"
}
}
class SimpleMessageResponse(BaseModel):
"""简单消息响应模型"""
message: str = Field(..., description="操作消息")
class Config:
json_schema_extra = {
"example": {
"message": "删除成功"
}
}
class BatchOperationResponse(BaseModel):
"""批量操作响应模型"""
message: str = Field(..., description="操作消息")
updated_count: Optional[int] = Field(None, description="成功更新的数量")
deleted_count: Optional[int] = Field(None, description="成功删除的数量")
failed_ids: Optional[List[int]] = Field(None, description="失败的ID列表")
errors: Optional[dict] = Field(None, description="错误详情")
class Config:
json_schema_extra = {
"example": {
"message": "批量删除成功",
"deleted_count": 2,
"failed_ids": [11],
"errors": {
"11": "该分组下存在子分组,无法删除"
}
}
}
class ErrorResponse(BaseModel):
"""错误响应模型"""
detail: str = Field(..., description="错误详情")
code: Optional[int] = Field(None, description="错误代码")
class Config:
json_schema_extra = {
"example": {
"detail": "该分组不存在",
"code": 404
}
}
TypeScript (前端) 接口定义
/**
* 评查点分组基础接口
*/
export interface EvaluationPointGroupResponse {
id: number;
pid: number | null;
name: string;
code: string;
description: string | null;
is_enabled: boolean;
created_at: string; // ISO 8601 format
updated_at: string; // ISO 8601 format
rule_count?: number | null;
children?: EvaluationPointGroupResponse[] | null;
}
/**
* 列表响应接口
*/
export interface EvaluationPointGroupListResponse {
data: EvaluationPointGroupResponse[];
total: number;
page: number;
page_size: number;
}
/**
* 树形结构响应接口
*/
export interface EvaluationPointGroupTreeResponse {
data: EvaluationPointGroupResponse[];
}
/**
* 详情响应接口
*/
export interface EvaluationPointGroupDetailResponse {
data: EvaluationPointGroupResponse;
}
/**
* 创建/更新请求接口
*/
export interface EvaluationPointGroupCreateUpdateDto {
pid?: number | null;
name: string;
code: string;
description?: string | null;
is_enabled: boolean;
}
/**
* 创建/更新响应接口
*/
export interface EvaluationPointGroupCreateUpdateResponse {
data: EvaluationPointGroupResponse;
message: string;
}
/**
* 批量更新状态请求接口
*/
export interface BatchUpdateStatusRequest {
ids: number[];
is_enabled: boolean;
}
/**
* 批量删除请求接口
*/
export interface BatchDeleteRequest {
ids: number[];
}
/**
* 批量操作响应接口
*/
export interface BatchOperationResponse {
message: string;
updated_count?: number;
deleted_count?: number;
failed_ids?: number[];
errors?: Record<string, string>;
}
/**
* 错误响应接口
*/
export interface ErrorResponse {
detail: string;
code?: number;
}
接口列表
| 序号 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 1 | GET | /api/v3/evaluation-point-groups |
获取一级分组列表(分页) |
| 2 | GET | /api/v3/evaluation-point-groups/all |
获取完整树形结构 |
| 3 | GET | /api/v3/evaluation-point-groups/{id} |
获取单个分组详情 |
| 4 | GET | /api/v3/evaluation-point-groups/{parent_id}/children |
获取子分组列表(分页) |
| 5 | POST | /api/v3/evaluation-point-groups |
创建分组 |
| 6 | PUT | /api/v3/evaluation-point-groups/{id} |
更新分组 |
| 7 | DELETE | /api/v3/evaluation-point-groups/{id} |
删除分组 |
| 8 | PATCH | /api/v3/evaluation-point-groups/batch/status |
批量更新状态 |
| 9 | DELETE | /api/v3/evaluation-point-groups/batch |
批量删除 |
详细接口说明
1. 获取一级分组列表(分页)
请求
GET /api/v3/evaluation-point-groups?pid=null&page=1&page_size=20&name=合同&code=CONTRACT&is_enabled=true
Authorization: Bearer {jwt_token}
查询参数
| 参数 | 类型 | 必填 | 说明 | 默认值 |
|---|---|---|---|---|
| pid | string | 否 | 父分组ID筛选 - pid=null:只查询一级分组(推荐)- 不传参数:查询所有分组 - pid=60:查询指定父分组的子分组 |
- |
| page | integer | 否 | 页码(从1开始) | 1 |
| page_size | integer | 否 | 每页记录数 | 20 |
| name | string | 否 | 分组名称模糊搜索 | - |
| code | string | 否 | 分组编码模糊搜索 | - |
| is_enabled | boolean | 否 | 是否启用筛选 | - |
⚠️ 重要说明:pid 参数处理
前端会传递字符串 "null" 来表示查询一级分组,后端需要识别并转换:
# FastAPI 后端处理示例
@app.get("/api/v3/evaluation-point-groups")
async def get_evaluation_point_groups(
pid: Optional[str] = None, # 接收字符串类型
page: int = 1,
page_size: int = 20,
name: Optional[str] = None,
code: Optional[str] = None,
is_enabled: Optional[bool] = None
):
# 🔑 将字符串 "null" 转换为 None
if pid == "null":
pid_value = None
elif pid is not None:
pid_value = int(pid)
else:
# 不传 pid 参数时,根据业务需求决定:
# 选项1: 查询所有分组
# 选项2: 默认查询一级分组(推荐)
pid_value = None
# 使用 pid_value 构建查询
query = db.query(EvaluationPointGroup)
if pid_value is None:
# 查询一级分组: WHERE pid IS NULL
query = query.filter(EvaluationPointGroup.pid.is_(None))
else:
# 查询指定父分组的子分组
query = query.filter(EvaluationPointGroup.pid == pid_value)
# ... 其他过滤条件
成功响应 (200)
{
"data": [
{
"id": 1,
"pid": null,
"name": "合同管理类",
"code": "CONTRACT_001",
"description": "合同管理相关的评查点分组",
"is_enabled": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"rule_count": 25
},
{
"id": 5,
"pid": null,
"name": "合同审核类",
"code": "CONTRACT_002",
"description": null,
"is_enabled": true,
"created_at": "2025-01-16T09:20:00Z",
"updated_at": "2025-01-16T09:20:00Z",
"rule_count": 18
}
],
"total": 2,
"page": 1,
"page_size": 20
}
错误响应 (401)
{
"detail": "未授权,请先登录",
"code": 401
}
2. 获取完整树形结构
请求
GET /api/v3/evaluation-point-groups/all?flat=false&include_disabled=true
Authorization: Bearer {jwt_token}
查询参数
| 参数 | 类型 | 必填 | 说明 | 默认值 |
|---|---|---|---|---|
| flat | boolean | 否 | 是否返回扁平结构(true:扁平数组,false:树形) | false |
| include_disabled | boolean | 否 | 是否包含已禁用的分组 | true |
成功响应 (200) - 树形结构 (flat=false)
{
"data": [
{
"id": 1,
"pid": null,
"name": "合同管理类",
"code": "CONTRACT_001",
"description": "合同管理相关的评查点分组",
"is_enabled": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"rule_count": 25,
"children": [
{
"id": 2,
"pid": 1,
"name": "合同审批",
"code": "CONTRACT_001_001",
"description": null,
"is_enabled": true,
"created_at": "2025-01-15T10:31:00Z",
"updated_at": "2025-01-15T10:31:00Z",
"rule_count": 10,
"children": [
{
"id": 3,
"pid": 2,
"name": "审批流程",
"code": "CONTRACT_001_001_001",
"description": null,
"is_enabled": true,
"created_at": "2025-01-15T10:32:00Z",
"updated_at": "2025-01-15T10:32:00Z",
"rule_count": 5,
"children": []
}
]
},
{
"id": 4,
"pid": 1,
"name": "合同签署",
"code": "CONTRACT_001_002",
"description": null,
"is_enabled": true,
"created_at": "2025-01-15T10:33:00Z",
"updated_at": "2025-01-15T10:33:00Z",
"rule_count": 15,
"children": []
}
]
}
]
}
成功响应 (200) - 扁平结构 (flat=true)
{
"data": [
{
"id": 1,
"pid": null,
"name": "合同管理类",
"code": "CONTRACT_001",
"description": "合同管理相关的评查点分组",
"is_enabled": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"rule_count": 25
},
{
"id": 2,
"pid": 1,
"name": "合同审批",
"code": "CONTRACT_001_001",
"description": null,
"is_enabled": true,
"created_at": "2025-01-15T10:31:00Z",
"updated_at": "2025-01-15T10:31:00Z",
"rule_count": 10
},
{
"id": 3,
"pid": 2,
"name": "审批流程",
"code": "CONTRACT_001_001_001",
"description": null,
"is_enabled": true,
"created_at": "2025-01-15T10:32:00Z",
"updated_at": "2025-01-15T10:32:00Z",
"rule_count": 5
}
]
}
3. 获取单个分组详情
请求
GET /api/v3/evaluation-point-groups/1?include_children=true
Authorization: Bearer {jwt_token}
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | integer | 是 | 分组ID |
查询参数
| 参数 | 类型 | 必填 | 说明 | 默认值 |
|---|---|---|---|---|
| include_children | boolean | 否 | 是否包含子分组 | false |
成功响应 (200) - include_children=true
{
"data": {
"id": 1,
"pid": null,
"name": "合同管理类",
"code": "CONTRACT_001",
"description": "合同管理相关的评查点分组",
"is_enabled": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"rule_count": 25,
"children": [
{
"id": 2,
"pid": 1,
"name": "合同审批",
"code": "CONTRACT_001_001",
"description": null,
"is_enabled": true,
"created_at": "2025-01-15T10:31:00Z",
"updated_at": "2025-01-15T10:31:00Z",
"rule_count": 10,
"children": []
}
]
}
}
成功响应 (200) - include_children=false
{
"data": {
"id": 1,
"pid": null,
"name": "合同管理类",
"code": "CONTRACT_001",
"description": "合同管理相关的评查点分组",
"is_enabled": true,
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-01-15T10:30:00Z",
"rule_count": 25
}
}
错误响应 (404)
{
"detail": "分组不存在",
"code": 404
}
4. 获取子分组列表(分页)
请求
GET /api/v3/evaluation-point-groups/1/children?page=1&page_size=20
Authorization: Bearer {jwt_token}
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| parent_id | integer | 是 | 父分组ID |
查询参数
| 参数 | 类型 | 必填 | 说明 | 默认值 |
|---|---|---|---|---|
| page | integer | 否 | 页码(从1开始) | 1 |
| page_size | integer | 否 | 每页记录数 | 20 |
成功响应 (200)
{
"data": [
{
"id": 2,
"pid": 1,
"name": "合同审批",
"code": "CONTRACT_001_001",
"description": null,
"is_enabled": true,
"created_at": "2025-01-15T10:31:00Z",
"updated_at": "2025-01-15T10:31:00Z",
"rule_count": 10
},
{
"id": 4,
"pid": 1,
"name": "合同签署",
"code": "CONTRACT_001_002",
"description": null,
"is_enabled": true,
"created_at": "2025-01-15T10:33:00Z",
"updated_at": "2025-01-15T10:33:00Z",
"rule_count": 15
}
],
"total": 2,
"page": 1,
"page_size": 20
}
错误响应 (404)
{
"detail": "父分组不存在",
"code": 404
}
5. 创建分组
请求
POST /api/v3/evaluation-point-groups
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
"pid": 1,
"name": "新分组名称",
"code": "NEW_GROUP_001",
"description": "分组描述",
"is_enabled": true
}
请求体
| 字段 | 类型 | 必填 | 说明 | 限制 |
|---|---|---|---|---|
| pid | integer/null | 否 | 父分组ID,null表示创建一级分组 | 必须是存在的分组ID |
| name | string | 是 | 分组名称 | 1-100字符,不能为空 |
| code | string | 是 | 分组编码 | 1-50字符,必须唯一 |
| description | string/null | 否 | 分组描述 | 最多500字符 |
| is_enabled | boolean | 是 | 是否启用 | - |
成功响应 (201)
{
"data": {
"id": 100,
"pid": 1,
"name": "新分组名称",
"code": "NEW_GROUP_001",
"description": "分组描述",
"is_enabled": true,
"created_at": "2025-01-20T14:30:00Z",
"updated_at": "2025-01-20T14:30:00Z",
"rule_count": 0
},
"message": "创建成功"
}
错误响应 (400) - 编码重复
{
"detail": "分组编码已存在",
"code": 400
}
错误响应 (404) - 父分组不存在
{
"detail": "父分组不存在",
"code": 404
}
错误响应 (422) - 验证失败
{
"detail": [
{
"loc": ["body", "name"],
"msg": "分组名称不能为空",
"type": "value_error"
}
]
}
6. 更新分组
请求
PUT /api/v3/evaluation-point-groups/100
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
"pid": 1,
"name": "更新后的名称",
"code": "UPDATED_001",
"description": "更新后的描述",
"is_enabled": false
}
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | integer | 是 | 分组ID |
请求体
| 字段 | 类型 | 必填 | 说明 | 限制 |
|---|---|---|---|---|
| pid | integer/null | 否 | 父分组ID | 不能设置为自己或自己的子孙节点 |
| name | string | 是 | 分组名称 | 1-100字符 |
| code | string | 是 | 分组编码 | 1-50字符,不能与其他分组重复 |
| description | string/null | 否 | 分组描述 | 最多500字符 |
| is_enabled | boolean | 是 | 是否启用 | - |
成功响应 (200)
{
"data": {
"id": 100,
"pid": 1,
"name": "更新后的名称",
"code": "UPDATED_001",
"description": "更新后的描述",
"is_enabled": false,
"created_at": "2025-01-20T14:30:00Z",
"updated_at": "2025-01-20T14:35:00Z",
"rule_count": 0
},
"message": "更新成功"
}
错误响应 (400) - 父分组设置错误
{
"detail": "不能将分组设置为自己的子孙节点的父分组",
"code": 400
}
错误响应 (404)
{
"detail": "分组不存在",
"code": 404
}
7. 删除分组
请求
DELETE /api/v3/evaluation-point-groups/100
Authorization: Bearer {jwt_token}
路径参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | integer | 是 | 分组ID |
成功响应 (200)
{
"message": "删除成功"
}
错误响应 (400) - 存在子分组
{
"detail": "该分组下存在子分组,无法删除",
"code": 400
}
错误响应 (400) - 存在关联评查点
{
"detail": "该分组下存在评查点,无法删除",
"code": 400
}
错误响应 (404)
{
"detail": "分组不存在",
"code": 404
}
8. 批量更新状态
请求
PATCH /api/v3/evaluation-point-groups/batch/status
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
"ids": [1, 2, 3],
"is_enabled": false
}
请求体
| 字段 | 类型 | 必填 | 说明 | 限制 |
|---|---|---|---|---|
| ids | integer[] | 是 | 要更新的分组ID列表 | 至少包含1个ID |
| is_enabled | boolean | 是 | 目标状态 | - |
成功响应 (200)
{
"message": "批量更新成功",
"updated_count": 3
}
部分成功响应 (200)
{
"message": "部分更新成功",
"updated_count": 2,
"failed_ids": [999],
"errors": {
"999": "分组不存在"
}
}
错误响应 (400)
{
"detail": "ids 不能为空",
"code": 400
}
9. 批量删除
请求
DELETE /api/v3/evaluation-point-groups/batch
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
"ids": [10, 11, 12]
}
请求体
| 字段 | 类型 | 必填 | 说明 | 限制 |
|---|---|---|---|---|
| ids | integer[] | 是 | 要删除的分组ID列表 | 至少包含1个ID |
成功响应 (200)
{
"message": "批量删除成功",
"deleted_count": 3
}
部分成功响应 (200)
{
"message": "部分删除成功",
"deleted_count": 2,
"failed_ids": [11],
"errors": {
"11": "该分组下存在子分组,无法删除"
}
}
错误响应 (400)
{
"detail": "ids 不能为空",
"code": 400
}
错误处理规范
HTTP 状态码
| 状态码 | 说明 | 使用场景 |
|---|---|---|
| 200 | OK | 请求成功 |
| 201 | Created | 资源创建成功 |
| 400 | Bad Request | 请求参数错误、业务逻辑错误 |
| 401 | Unauthorized | 未授权,JWT token无效或过期 |
| 403 | Forbidden | 无权限访问 |
| 404 | Not Found | 资源不存在 |
| 422 | Unprocessable Entity | 数据验证失败 |
| 500 | Internal Server Error | 服务器内部错误 |
错误响应格式
标准错误格式
{
"detail": "错误描述信息",
"code": 400
}
验证错误格式 (422)
{
"detail": [
{
"loc": ["body", "name"],
"msg": "分组名称不能为空",
"type": "value_error"
},
{
"loc": ["body", "code"],
"msg": "分组编码长度必须在1-50之间",
"type": "value_error"
}
]
}
常见错误代码
| 错误代码 | 错误信息 | HTTP状态码 |
|---|---|---|
| 40001 | 分组编码已存在 | 400 |
| 40002 | 该分组下存在子分组,无法删除 | 400 |
| 40003 | 该分组下存在评查点,无法删除 | 400 |
| 40004 | 不能将分组设置为自己的子孙节点的父分组 | 400 |
| 40101 | 未授权,请先登录 | 401 |
| 40102 | Token已过期 | 401 |
| 40103 | Token无效 | 401 |
| 40401 | 分组不存在 | 404 |
| 40402 | 父分组不存在 | 404 |
验证规则
字段验证规则
| 字段 | 验证规则 |
|---|---|
| name | - 必填 - 长度:1-100字符 - 不能只包含空格 - 去除首尾空格后存储 |
| code | - 必填 - 长度:1-50字符 - 唯一性校验 - 自动转换为大写 - 建议格式:字母数字下划线 |
| description | - 可选 - 最大长度:500字符 |
| is_enabled | - 必填 - 布尔值 |
| pid | - 可选 - 必须是存在的分组ID - 不能是自己 - 不能是自己的子孙节点 |
业务规则
-
创建分组
- 编码必须唯一
- 父分组必须存在(如果指定了pid)
- 创建后 rule_count 初始为 0
-
更新分组
- 不能将自己设置为自己的子孙节点的父分组(避免循环引用)
- 编码不能与其他分组重复(可以与自己原来的编码相同)
- 更新时自动更新 updated_at
-
删除分组
- 不能删除有子分组的分组
- 不能删除有关联评查点的分组
- 建议:删除前检查并提示用户
-
rule_count 计算
- 包含该分组直接关联的评查点
- 包含所有子分组(递归)关联的评查点
- 在评查点创建/删除/移动时需要更新相关分组的 rule_count
完整测试用例
测试用例 1: 创建一级分组
请求
curl -X POST http://10.79.97.17:8000/api/v3/evaluation-point-groups \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{
"pid": null,
"name": "财务管理类",
"code": "FINANCE_001",
"description": "财务相关的评查点分组",
"is_enabled": true
}'
预期响应 (201)
{
"data": {
"id": 101,
"pid": null,
"name": "财务管理类",
"code": "FINANCE_001",
"description": "财务相关的评查点分组",
"is_enabled": true,
"created_at": "2025-01-20T15:30:00Z",
"updated_at": "2025-01-20T15:30:00Z",
"rule_count": 0
},
"message": "创建成功"
}
测试用例 2: 创建子分组
请求
curl -X POST http://10.79.97.17:8000/api/v3/evaluation-point-groups \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{
"pid": 101,
"name": "预算管理",
"code": "FINANCE_001_001",
"description": null,
"is_enabled": true
}'
预期响应 (201)
{
"data": {
"id": 102,
"pid": 101,
"name": "预算管理",
"code": "FINANCE_001_001",
"description": null,
"is_enabled": true,
"created_at": "2025-01-20T15:31:00Z",
"updated_at": "2025-01-20T15:31:00Z",
"rule_count": 0
},
"message": "创建成功"
}
测试用例 3: 获取树形结构
请求
curl -X GET "http://10.79.97.17:8000/api/v3/evaluation-point-groups/all?flat=false&include_disabled=false" \
-H "Authorization: Bearer eyJhbGc..."
预期响应 (200)
{
"data": [
{
"id": 101,
"pid": null,
"name": "财务管理类",
"code": "FINANCE_001",
"description": "财务相关的评查点分组",
"is_enabled": true,
"created_at": "2025-01-20T15:30:00Z",
"updated_at": "2025-01-20T15:30:00Z",
"rule_count": 0,
"children": [
{
"id": 102,
"pid": 101,
"name": "预算管理",
"code": "FINANCE_001_001",
"description": null,
"is_enabled": true,
"created_at": "2025-01-20T15:31:00Z",
"updated_at": "2025-01-20T15:31:00Z",
"rule_count": 0,
"children": []
}
]
}
]
}
测试用例 4: 更新分组(禁用)
请求
curl -X PUT http://10.79.97.17:8000/api/v3/evaluation-point-groups/102 \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{
"pid": 101,
"name": "预算管理",
"code": "FINANCE_001_001",
"description": "预算编制与执行",
"is_enabled": false
}'
预期响应 (200)
{
"data": {
"id": 102,
"pid": 101,
"name": "预算管理",
"code": "FINANCE_001_001",
"description": "预算编制与执行",
"is_enabled": false,
"created_at": "2025-01-20T15:31:00Z",
"updated_at": "2025-01-20T15:35:00Z",
"rule_count": 0
},
"message": "更新成功"
}
测试用例 5: 批量更新状态
请求
curl -X PATCH http://10.79.97.17:8000/api/v3/evaluation-point-groups/batch/status \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{
"ids": [101, 102],
"is_enabled": true
}'
预期响应 (200)
{
"message": "批量更新成功",
"updated_count": 2
}
测试用例 6: 删除分组(有子分组,应该失败)
请求
curl -X DELETE http://10.79.97.17:8000/api/v3/evaluation-point-groups/101 \
-H "Authorization: Bearer eyJhbGc..."
预期响应 (400)
{
"detail": "该分组下存在子分组,无法删除",
"code": 400
}
测试用例 7: 删除子分组(成功)
请求
curl -X DELETE http://10.79.97.17:8000/api/v3/evaluation-point-groups/102 \
-H "Authorization: Bearer eyJhbGc..."
预期响应 (200)
{
"message": "删除成功"
}
测试用例 8: 重复编码(应该失败)
请求
curl -X POST http://10.79.97.17:8000/api/v3/evaluation-point-groups \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{
"pid": null,
"name": "重复编码测试",
"code": "FINANCE_001",
"description": null,
"is_enabled": true
}'
预期响应 (400)
{
"detail": "分组编码已存在",
"code": 400
}
测试用例 9: 获取分页列表(带搜索)
请求
curl -X GET "http://10.79.97.17:8000/api/v3/evaluation-point-groups?page=1&page_size=10&name=财务&is_enabled=true" \
-H "Authorization: Bearer eyJhbGc..."
预期响应 (200)
{
"data": [
{
"id": 101,
"pid": null,
"name": "财务管理类",
"code": "FINANCE_001",
"description": "财务相关的评查点分组",
"is_enabled": true,
"created_at": "2025-01-20T15:30:00Z",
"updated_at": "2025-01-20T15:30:00Z",
"rule_count": 0
}
],
"total": 1,
"page": 1,
"page_size": 10
}
测试用例 10: 批量删除
请求
curl -X DELETE http://10.79.97.17:8000/api/v3/evaluation-point-groups/batch \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{
"ids": [101, 999]
}'
预期响应 (200) - 部分成功
{
"message": "部分删除成功",
"deleted_count": 1,
"failed_ids": [999],
"errors": {
"999": "分组不存在"
}
}
附录
数据库表结构建议
CREATE TABLE evaluation_point_groups (
id SERIAL PRIMARY KEY,
pid INTEGER REFERENCES evaluation_point_groups(id) ON DELETE RESTRICT,
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL UNIQUE,
description TEXT,
is_enabled BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- 索引
CREATE INDEX idx_evaluation_point_groups_pid ON evaluation_point_groups(pid);
CREATE INDEX idx_evaluation_point_groups_code ON evaluation_point_groups(code);
CREATE INDEX idx_evaluation_point_groups_is_enabled ON evaluation_point_groups(is_enabled);
CREATE INDEX idx_evaluation_point_groups_name ON evaluation_point_groups USING gin(to_tsvector('simple', name));
-- 触发器:自动更新 updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_evaluation_point_groups_updated_at
BEFORE UPDATE ON evaluation_point_groups
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
rule_count 计算SQL示例
-- 计算某个分组的 rule_count(包含所有子孙分组)
WITH RECURSIVE group_tree AS (
-- 基础查询:选择目标分组
SELECT id
FROM evaluation_point_groups
WHERE id = :group_id
UNION ALL
-- 递归查询:选择所有子孙分组
SELECT g.id
FROM evaluation_point_groups g
INNER JOIN group_tree gt ON g.pid = gt.id
)
SELECT COUNT(*) as rule_count
FROM evaluation_points ep
WHERE ep.group_id IN (SELECT id FROM group_tree);
版本历史
| 版本 | 日期 | 说明 |
|---|---|---|
| 1.0.0 | 2025-01-20 | 初始版本 |
联系方式
如有疑问,请联系后端开发团队。