# ============================================================================= # 企微IT智能服务台 — 代答排除管理 API # ============================================================================= # 说明:代答排除规则的管理后台接口,共 8 个端点。 # 路由前缀:/admin/exclusion-rules(router.py 已注册) # 认证:@require_admin # # 1. GET /admin/exclusion-rules — 规则列表(分页+筛选) # 2. POST /admin/exclusion-rules — 新建规则 # 3. GET /admin/exclusion-rules/{id} — 规则详情 # 4. PUT /admin/exclusion-rules/{id} — 编辑规则 # 5. DELETE /admin/exclusion-rules/{id} — 删除规则 # 6. POST /admin/exclusion-rules/{id}/toggle — 启用/停用 # 7. POST /admin/exclusion-rules/test — 测试匹配 # 8. GET /admin/exclusion-rules/stats — 统计概要 # ============================================================================= import logging from datetime import datetime from typing import Optional from fastapi import APIRouter, Depends, Query from sqlalchemy import or_, select, func, and_ from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.dependencies import get_current_user, require_admin, UserInfo from app.models.exclusion_rule import ExclusionRule from app.schemas.exclusion import ( ExclusionRuleCreate, ExclusionRuleUpdate, ExclusionRuleToggle, ExclusionTestRequest, ExclusionRuleResponse, ExclusionTestResponse, ExclusionStatsResponse, ) from app.services.exclusion_service import get_exclusion_service logger = logging.getLogger(__name__) router = APIRouter() # ============================================================================= # 工具函数 # ============================================================================= def _rule_to_response(rule: ExclusionRule) -> dict: """将 ORM 对象转为响应字典。""" return { "id": rule.id, "rule_name": rule.rule_name, "rule_description": rule.rule_description, "priority": rule.priority, "match_type": rule.match_type, "match_condition": rule.match_condition, "match_scope": rule.match_scope or [], "action_type": rule.action_type, "transfer_message": rule.transfer_message, "status": rule.status, "hit_count": rule.hit_count, "created_by": rule.created_by, "created_at": rule.created_at.isoformat() if rule.created_at else None, "updated_at": rule.updated_at.isoformat() if rule.updated_at else None, } # ============================================================================= # 1. 规则列表(分页+筛选) # ============================================================================= @router.get("") @require_admin async def list_rules( status: Optional[str] = Query(default=None, description="状态筛选:enabled/disabled"), match_type: Optional[str] = Query(default=None, description="匹配方式筛选"), priority: Optional[str] = Query(default=None, description="优先级筛选:P0/P1/P2/P3"), keyword: Optional[str] = Query(default=None, description="关键词搜索(规则名称/描述)"), page: int = Query(default=1, ge=1, description="页码"), page_size: int = Query(default=20, ge=1, le=100, description="每页数量"), current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """获取排除规则列表(分页+筛选)。 支持按状态、匹配方式、优先级筛选,以及关键词搜索规则名称和描述。 """ conditions = [] if status: conditions.append(ExclusionRule.status == status) if match_type: conditions.append(ExclusionRule.match_type == match_type) if priority: conditions.append(ExclusionRule.priority == priority) if keyword: conditions.append( or_( ExclusionRule.rule_name.ilike(f"%{keyword}%"), ExclusionRule.rule_description.ilike(f"%{keyword}%"), ) ) # 统计总数 count_stmt = select(func.count()).select_from(ExclusionRule) if conditions: count_stmt = count_stmt.where(and_(*conditions)) total_result = await db.execute(count_stmt) total = total_result.scalar() or 0 # 分页查询 stmt = select(ExclusionRule).order_by( ExclusionRule.priority, ExclusionRule.created_at.desc(), ) if conditions: stmt = stmt.where(and_(*conditions)) offset = (page - 1) * page_size stmt = stmt.offset(offset).limit(page_size) result = await db.execute(stmt) rules = result.scalars().all() return { "code": 0, "message": "success", "data": { "total": total, "items": [_rule_to_response(r) for r in rules], }, } # ============================================================================= # 2. 新建规则 # ============================================================================= @router.post("") @require_admin async def create_rule( body: ExclusionRuleCreate, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """新建排除规则。""" # 检查规则名称唯一性 existing = await db.execute( select(ExclusionRule).where(ExclusionRule.rule_name == body.rule_name) ) if existing.scalar_one_or_none(): return {"code": 400, "message": f"规则名称已存在: {body.rule_name}", "data": None} rule = ExclusionRule( rule_name=body.rule_name, rule_description=body.rule_description, priority=body.priority, match_type=body.match_type, match_condition=body.match_condition, match_scope=body.match_scope, action_type=body.action_type, transfer_message=body.transfer_message, status="enabled", hit_count=0, created_by=current_user.employee_id, ) db.add(rule) await db.commit() await db.refresh(rule) logger.info("排除规则已创建: %s, by=%s", rule.rule_name, current_user.employee_id) return { "code": 0, "message": "规则创建成功", "data": _rule_to_response(rule), } # ============================================================================= # 3. 测试匹配(必须在 /{rule_id} 之前注册,避免路由冲突) # ============================================================================= @router.post("/test") @require_admin async def test_match( body: ExclusionTestRequest, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """测试消息匹配排除规则。 可指定 rule_id 测试单条规则,不指定则测试所有启用规则。 测试不会记录日志、不会更新 hit_count。 """ service = get_exclusion_service() result = await service.test_match( db=db, message=body.message, rule_id=body.rule_id, ) return { "code": 0, "message": "success", "data": { "matched": result.matched, "matched_detail": result.matched_detail if result.matched else None, "rule_name": result.rule_name if result.matched else None, "action_type": result.action_type if result.matched else None, }, } # ============================================================================= # 4. 统计概要(必须在 /{rule_id} 之前注册,避免路由冲突) # ============================================================================= @router.get("/stats") @require_admin async def get_stats( current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """获取排除规则统计概要。""" service = get_exclusion_service() stats = await service.get_stats(db=db) return { "code": 0, "message": "success", "data": stats, } # ============================================================================= # 5. 规则详情 # 注意:/test 和 /stats 必须在此路由之前注册,否则会被 /{rule_id} 匹配 # ============================================================================= @router.get("/{rule_id}") @require_admin async def get_rule( rule_id: str, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """获取规则详情。""" result = await db.execute( select(ExclusionRule).where(ExclusionRule.id == rule_id) ) rule = result.scalar_one_or_none() if not rule: return {"code": 404, "message": "规则不存在", "data": None} return { "code": 0, "message": "success", "data": _rule_to_response(rule), } # ============================================================================= # 6. 编辑规则 # ============================================================================= @router.put("/{rule_id}") @require_admin async def update_rule( rule_id: str, body: ExclusionRuleUpdate, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """编辑排除规则。""" result = await db.execute( select(ExclusionRule).where(ExclusionRule.id == rule_id) ) rule = result.scalar_one_or_none() if not rule: return {"code": 404, "message": "规则不存在", "data": None} # 如果修改了规则名称,检查唯一性 if body.rule_name and body.rule_name != rule.rule_name: existing = await db.execute( select(ExclusionRule).where(ExclusionRule.rule_name == body.rule_name) ) if existing.scalar_one_or_none(): return {"code": 400, "message": f"规则名称已存在: {body.rule_name}", "data": None} # 更新字段(仅更新传入的字段) update_data = body.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(rule, field, value) rule.updated_at = datetime.now() await db.commit() await db.refresh(rule) logger.info("排除规则已更新: %s, by=%s", rule.rule_name, current_user.employee_id) return { "code": 0, "message": "规则更新成功", "data": _rule_to_response(rule), } # ============================================================================= # 7. 删除规则 # ============================================================================= @router.delete("/{rule_id}") @require_admin async def delete_rule( rule_id: str, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """删除排除规则。""" result = await db.execute( select(ExclusionRule).where(ExclusionRule.id == rule_id) ) rule = result.scalar_one_or_none() if not rule: return {"code": 404, "message": "规则不存在", "data": None} rule_name = rule.rule_name await db.delete(rule) await db.commit() logger.info("排除规则已删除: %s, by=%s", rule_name, current_user.employee_id) return { "code": 0, "message": "删除成功", "data": None, } # ============================================================================= # 8. 启用/停用 # ============================================================================= @router.post("/{rule_id}/toggle") @require_admin async def toggle_rule( rule_id: str, body: ExclusionRuleToggle, current_user: UserInfo = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """启用/停用排除规则。""" result = await db.execute( select(ExclusionRule).where(ExclusionRule.id == rule_id) ) rule = result.scalar_one_or_none() if not rule: return {"code": 404, "message": "规则不存在", "data": None} if body.status not in ("enabled", "disabled"): return {"code": 400, "message": "无效状态,仅支持 enabled/disabled", "data": None} rule.status = body.status rule.updated_at = datetime.now() await db.commit() await db.refresh(rule) logger.info( "排除规则状态切换: %s → %s, by=%s", rule.rule_name, rule.status, current_user.employee_id, ) return { "code": 0, "message": f"规则已{'启用' if rule.status == 'enabled' else '停用'}", "data": _rule_to_response(rule), } # 注意:/test 和 /stats 路由已在文件上方(/{rule_id} 之前)注册, # 避免 FastAPI 路由匹配将 "test"/"stats" 误认为 rule_id。