Files
leaudit-platform-frontend/docs/evaluation/evaluation_point_groups_backend_api_spec.md
TanWenyan e7646d17a6 fix(evaluation-groups): 修复一级分组显示错误和 React key 警告
## 修复内容

### 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
2025-11-26 10:05:39 +08:00

38 KiB
Raw Permalink Blame History

评查点分组 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="父分组IDnull表示一级分组")
    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="父分组IDnull表示创建一级分组")
    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 父分组IDnull表示创建一级分组 必须是存在的分组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
- 不能是自己
- 不能是自己的子孙节点

业务规则

  1. 创建分组

    • 编码必须唯一
    • 父分组必须存在(如果指定了pid
    • 创建后 rule_count 初始为 0
  2. 更新分组

    • 不能将自己设置为自己的子孙节点的父分组(避免循环引用)
    • 编码不能与其他分组重复(可以与自己原来的编码相同)
    • 更新时自动更新 updated_at
  3. 删除分组

    • 不能删除有子分组的分组
    • 不能删除有关联评查点的分组
    • 建议:删除前检查并提示用户
  4. 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 初始版本

联系方式

如有疑问,请联系后端开发团队。