diff --git a/app/api/evaluation_points/rule-groups.ts b/app/api/evaluation_points/rule-groups.ts
index 5c6f58b..057cb0e 100644
--- a/app/api/evaluation_points/rule-groups.ts
+++ b/app/api/evaluation_points/rule-groups.ts
@@ -1171,6 +1171,7 @@ export async function getEvaluationPointGroups(
name,
code,
is_enabled,
+ pid,
orderBy = 'created_at',
order = 'desc'
} = params || {};
@@ -1182,6 +1183,19 @@ export async function getEvaluationPointGroups(
if (name) queryParams.append('name', name);
if (code) queryParams.append('code', code);
if (is_enabled !== undefined) queryParams.append('is_enabled', String(is_enabled));
+ // 🔑 添加 pid 参数过滤
+ // pid=null 或 pid='0' 表示只查询一级分组,后端需要识别字符串 "null"
+ // 如果 pid 未定义,则不传该参数(默认查询所有分组)
+ if (pid !== undefined) {
+ if (pid === null || pid === '0') {
+ // 方案1:传递字符串 "null",后端需要识别并转换为 None/NULL
+ queryParams.append('pid', 'null');
+ // 方案2:不传参数,后端默认查询一级分组(需要后端支持)
+ // 不添加 pid 参数
+ } else {
+ queryParams.append('pid', String(pid));
+ }
+ }
const url = `/api/v3/evaluation-point-groups?${queryParams.toString()}`;
diff --git a/app/routes/rule-groups._index.tsx b/app/routes/rule-groups._index.tsx
index 90cb9f2..8774184 100644
--- a/app/routes/rule-groups._index.tsx
+++ b/app/routes/rule-groups._index.tsx
@@ -814,7 +814,7 @@ export default function RuleGroupsIndex() {
record.isParent ? `parent-${record.id}` : `child-${record.id}`}
emptyText="暂无分组数据"
className="tree-table"
/>
diff --git a/docs/evaluation/evaluation_point_groups_backend_api_spec.md b/docs/evaluation/evaluation_point_groups_backend_api_spec.md
new file mode 100644
index 0000000..a8093cb
--- /dev/null
+++ b/docs/evaluation/evaluation_point_groups_backend_api_spec.md
@@ -0,0 +1,1568 @@
+# 评查点分组 FastAPI v3 后端接口规范
+
+## 📚 目录
+- [数据模型定义](#数据模型定义)
+- [接口列表](#接口列表)
+- [详细接口说明](#详细接口说明)
+- [错误处理规范](#错误处理规范)
+- [验证规则](#验证规则)
+- [完整测试用例](#完整测试用例)
+
+---
+
+## 数据模型定义
+
+### Python (FastAPI/Pydantic) 模型
+
+```python
+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 (前端) 接口定义
+
+```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;
+}
+
+/**
+ * 错误响应接口
+ */
+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. 获取一级分组列表(分页)
+
+**请求**
+```http
+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"` 来表示查询一级分组,后端需要识别并转换:
+
+```python
+# 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)**
+```json
+{
+ "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)**
+```json
+{
+ "detail": "未授权,请先登录",
+ "code": 401
+}
+```
+
+---
+
+### 2. 获取完整树形结构
+
+**请求**
+```http
+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)**
+```json
+{
+ "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)**
+```json
+{
+ "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. 获取单个分组详情
+
+**请求**
+```http
+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**
+```json
+{
+ "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**
+```json
+{
+ "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)**
+```json
+{
+ "detail": "分组不存在",
+ "code": 404
+}
+```
+
+---
+
+### 4. 获取子分组列表(分页)
+
+**请求**
+```http
+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)**
+```json
+{
+ "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)**
+```json
+{
+ "detail": "父分组不存在",
+ "code": 404
+}
+```
+
+---
+
+### 5. 创建分组
+
+**请求**
+```http
+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)**
+```json
+{
+ "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) - 编码重复**
+```json
+{
+ "detail": "分组编码已存在",
+ "code": 400
+}
+```
+
+**错误响应 (404) - 父分组不存在**
+```json
+{
+ "detail": "父分组不存在",
+ "code": 404
+}
+```
+
+**错误响应 (422) - 验证失败**
+```json
+{
+ "detail": [
+ {
+ "loc": ["body", "name"],
+ "msg": "分组名称不能为空",
+ "type": "value_error"
+ }
+ ]
+}
+```
+
+---
+
+### 6. 更新分组
+
+**请求**
+```http
+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)**
+```json
+{
+ "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) - 父分组设置错误**
+```json
+{
+ "detail": "不能将分组设置为自己的子孙节点的父分组",
+ "code": 400
+}
+```
+
+**错误响应 (404)**
+```json
+{
+ "detail": "分组不存在",
+ "code": 404
+}
+```
+
+---
+
+### 7. 删除分组
+
+**请求**
+```http
+DELETE /api/v3/evaluation-point-groups/100
+Authorization: Bearer {jwt_token}
+```
+
+**路径参数**
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| id | integer | 是 | 分组ID |
+
+**成功响应 (200)**
+```json
+{
+ "message": "删除成功"
+}
+```
+
+**错误响应 (400) - 存在子分组**
+```json
+{
+ "detail": "该分组下存在子分组,无法删除",
+ "code": 400
+}
+```
+
+**错误响应 (400) - 存在关联评查点**
+```json
+{
+ "detail": "该分组下存在评查点,无法删除",
+ "code": 400
+}
+```
+
+**错误响应 (404)**
+```json
+{
+ "detail": "分组不存在",
+ "code": 404
+}
+```
+
+---
+
+### 8. 批量更新状态
+
+**请求**
+```http
+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)**
+```json
+{
+ "message": "批量更新成功",
+ "updated_count": 3
+}
+```
+
+**部分成功响应 (200)**
+```json
+{
+ "message": "部分更新成功",
+ "updated_count": 2,
+ "failed_ids": [999],
+ "errors": {
+ "999": "分组不存在"
+ }
+}
+```
+
+**错误响应 (400)**
+```json
+{
+ "detail": "ids 不能为空",
+ "code": 400
+}
+```
+
+---
+
+### 9. 批量删除
+
+**请求**
+```http
+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)**
+```json
+{
+ "message": "批量删除成功",
+ "deleted_count": 3
+}
+```
+
+**部分成功响应 (200)**
+```json
+{
+ "message": "部分删除成功",
+ "deleted_count": 2,
+ "failed_ids": [11],
+ "errors": {
+ "11": "该分组下存在子分组,无法删除"
+ }
+}
+```
+
+**错误响应 (400)**
+```json
+{
+ "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 | 服务器内部错误 |
+
+### 错误响应格式
+
+**标准错误格式**
+```json
+{
+ "detail": "错误描述信息",
+ "code": 400
+}
+```
+
+**验证错误格式 (422)**
+```json
+{
+ "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: 创建一级分组
+
+**请求**
+```bash
+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)**
+```json
+{
+ "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: 创建子分组
+
+**请求**
+```bash
+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)**
+```json
+{
+ "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: 获取树形结构
+
+**请求**
+```bash
+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)**
+```json
+{
+ "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: 更新分组(禁用)
+
+**请求**
+```bash
+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)**
+```json
+{
+ "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: 批量更新状态
+
+**请求**
+```bash
+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)**
+```json
+{
+ "message": "批量更新成功",
+ "updated_count": 2
+}
+```
+
+---
+
+### 测试用例 6: 删除分组(有子分组,应该失败)
+
+**请求**
+```bash
+curl -X DELETE http://10.79.97.17:8000/api/v3/evaluation-point-groups/101 \
+ -H "Authorization: Bearer eyJhbGc..."
+```
+
+**预期响应 (400)**
+```json
+{
+ "detail": "该分组下存在子分组,无法删除",
+ "code": 400
+}
+```
+
+---
+
+### 测试用例 7: 删除子分组(成功)
+
+**请求**
+```bash
+curl -X DELETE http://10.79.97.17:8000/api/v3/evaluation-point-groups/102 \
+ -H "Authorization: Bearer eyJhbGc..."
+```
+
+**预期响应 (200)**
+```json
+{
+ "message": "删除成功"
+}
+```
+
+---
+
+### 测试用例 8: 重复编码(应该失败)
+
+**请求**
+```bash
+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)**
+```json
+{
+ "detail": "分组编码已存在",
+ "code": 400
+}
+```
+
+---
+
+### 测试用例 9: 获取分页列表(带搜索)
+
+**请求**
+```bash
+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)**
+```json
+{
+ "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: 批量删除
+
+**请求**
+```bash
+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) - 部分成功**
+```json
+{
+ "message": "部分删除成功",
+ "deleted_count": 1,
+ "failed_ids": [999],
+ "errors": {
+ "999": "分组不存在"
+ }
+}
+```
+
+---
+
+## 附录
+
+### 数据库表结构建议
+
+```sql
+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示例
+
+```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 | 初始版本 |
+
+---
+
+## 联系方式
+
+如有疑问,请联系后端开发团队。