Files
qihuaiflow/backend/python/app/api/admin/skills_admin.py
T
QihuAI Design 8942339744 fix(skills-admin): get_skills_stats 合并 market_apps(type=skill) 统计
修复顶部卡片仍显示 0 的问题:list_skills 已支持合并查询,
但 stats/overview 端点仍只读 evolution_skills。
新增对 market_apps 的统计: published→main, 其它→baby。
验证: total_skills=50, by_state.main=50, total_published=50
2026-07-16 15:39:21 +08:00

899 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
技能管理系统 API(管理员)
全平台技能管理:列表/创建/编辑/删除/状态管理/版本历史/发布市场
"""
import os
import json
import uuid
import logging
from datetime import datetime
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import desc, func, or_, and_, text
from ...database import get_db
from ...models import (
User, EvolutionSkill, SkillVersion, SkillPublishRecord,
EvolutionEvent, TeamSharedSkill, MarketApp, AppInstallation,
AppPurchase, AppReview
)
from ...auth import get_current_admin
from ...middleware.log_service import LogService
from ...config import UPLOAD_DIR
router = APIRouter()
logger = logging.getLogger(__name__)
# ============================================================
# 技能状态定义
# ============================================================
SKILL_STATES = {
"baby": {"label": "萌芽期", "color": "#9CA3AF", "description": "刚创建的技能,需要继续培养"},
"main": {"label": "主力期", "color": "#10B981", "description": "已成熟可正常使用"},
"staging": {"label": "灰度期", "color": "#F59E0B", "description": "正在灰度测试中"},
"frozen": {"label": "已冻结", "color": "#EF4444", "description": "已停止使用"},
}
SKILL_CATEGORIES = [
{"id": "writing", "label": "文字创作"},
{"id": "coding", "label": "代码能力"},
{"id": "data_analysis", "label": "数据分析"},
{"id": "qa", "label": "知识问答"},
{"id": "automation", "label": "流程自动化"},
{"id": "creativity", "label": "创意生成"},
{"id": "communication", "label": "沟通协作"},
{"id": "reasoning", "label": "逻辑推理"},
{"id": "other", "label": "其他"},
]
# ============================================================
# 辅助函数
# ============================================================
def market_app_to_skill_dict(app: "MarketApp") -> dict:
"""将市场应用(MarketApp type=skill)转换为与 EvolutionSkill 一致的字典格式"""
status = (getattr(app, "status", None) or "pending").strip()
state = "main" if status == "published" else "baby"
raw_tags = getattr(app, "tags", None)
tags_list = []
if raw_tags:
try:
tags_list = json.loads(raw_tags) if isinstance(raw_tags, str) else list(raw_tags)
except Exception:
tags_list = [t.strip() for t in str(raw_tags).split(",")]
category = getattr(app, "category", None) or "other"
return {
"id": app.id,
"user_id": getattr(app, "publisher_id", None) or "system",
"skill_name": app.name,
"description": getattr(app, "description", None) or "",
"tags": tags_list,
"category": category,
"category_label": next((c["label"] for c in SKILL_CATEGORIES if c["id"] == category), category or "其他"),
"state": state,
"state_info": SKILL_STATES.get(state, SKILL_STATES["baby"]),
"use_count": getattr(app, "install_count", None) or 0,
"version": 1,
"skill_md_path": None,
"git_commit_sha": None,
"last_used_at": None,
"created_at": app.created_at.isoformat() if getattr(app, "created_at", None) else None,
"updated_at": getattr(app, "updated_at", None).isoformat() if getattr(app, "updated_at", None) else None,
"source": "market",
}
def skill_to_dict(s: EvolutionSkill, include_md: bool = False) -> dict:
"""将技能ORM对象转换为字典"""
result = {
"id": s.id,
"user_id": s.user_id,
"skill_name": s.skill_name,
"description": s.description,
"tags": s.tags or [],
"category": s.category,
"category_label": next((c["label"] for c in SKILL_CATEGORIES if c["id"] == s.category), s.category or "其他"),
"state": s.state,
"state_info": SKILL_STATES.get(s.state, SKILL_STATES["baby"]),
"use_count": s.use_count or 0,
"version": s.version or 1,
"skill_md_path": s.skill_md_path,
"git_commit_sha": s.git_commit_sha,
"last_used_at": s.last_used_at.isoformat() if s.last_used_at else None,
"created_at": s.created_at.isoformat() if s.created_at else None,
"updated_at": s.updated_at.isoformat() if s.updated_at else None,
}
# 读取 SKILL.md 内容
if include_md and s.skill_md_path and os.path.exists(s.skill_md_path):
try:
with open(s.skill_md_path, 'r', encoding='utf-8') as f:
result["skill_md_content"] = f.read()
except Exception:
result["skill_md_content"] = None
else:
result["skill_md_content"] = None
return result
def version_to_dict(v: SkillVersion) -> dict:
return {
"id": v.id,
"user_id": v.user_id,
"skill_name": v.skill_name,
"version": v.version,
"skill_md_content": v.skill_md_content,
"git_commit_sha": v.git_commit_sha,
"description": v.description,
"created_at": v.created_at.isoformat() if v.created_at else None,
}
# ============================================================
# 管理员技能列表
# ============================================================
@router.get("/")
async def list_skills(
state: str = Query(None, description="技能状态过滤: baby/main/staging/frozen"),
category: str = Query(None, description="技能分类过滤"),
keyword: str = Query(None, description="关键词搜索(名称/描述)"),
user_id: str = Query(None, description="按用户ID筛选"),
sort_by: str = Query("updated_at", description="排序字段"),
order: str = Query("desc", description="排序方向: asc/desc"),
page: int = Query(1, ge=1, description="页码"),
page_size: int = Query(20, ge=1, le=100, description="每页数量"),
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取全平台技能列表(支持分页/过滤/搜索)—— 同时查询 EvolutionSkill + MarketApp(type=skill)"""
logger.info(f"[Admin Skills] {admin_user.username} 列出技能: state={state}, keyword={keyword}")
result: list = []
# ===== 1) 查询 EvolutionSkill =====
evo_query = db.query(EvolutionSkill)
if state:
evo_query = evo_query.filter(EvolutionSkill.state == state)
if category:
evo_query = evo_query.filter(EvolutionSkill.category == category)
if keyword:
evo_query = evo_query.filter(
or_(EvolutionSkill.skill_name.contains(keyword), EvolutionSkill.description.contains(keyword))
)
if user_id:
evo_query = evo_query.filter(EvolutionSkill.user_id == user_id)
evo_skills = evo_query.all()
user_ids = list(set(s.user_id for s in evo_skills))
users_map = {u.id: u.username for u in db.query(User.id, User.username).filter(User.id.in_(user_ids)).all()}
for s in evo_skills:
item = skill_to_dict(s)
item["owner_username"] = users_map.get(s.user_id, s.user_id)
item["source"] = "evolution"
result.append(item)
# ===== 2) 查询 MarketApp (type=skill) =====
mkt_query = db.query(MarketApp).filter(MarketApp.type == "skill")
if keyword:
mkt_query = mkt_query.filter(
or_(MarketApp.name.contains(keyword), MarketApp.description.contains(keyword))
)
if category:
mkt_query = mkt_query.filter(MarketApp.category == category)
# 状态映射:主面板只展示已发布 → state='main';草稿/移除 → 'baby'
if state == "main":
mkt_query = mkt_query.filter(MarketApp.status == "published")
elif state:
mkt_query = mkt_query.filter(MarketApp.status != "published") # 非发布=草稿等
market_apps_rows = mkt_query.all()
for app in market_apps_rows:
item = market_app_to_skill_dict(app)
item["owner_username"] = app.publisher_name or "system"
result.append(item)
# ===== 3) 合并排序 & 分页 =====
total = len(result)
reverse = order == "desc"
key_fn = lambda x: x.get(sort_by) or ""
result.sort(key=key_fn, reverse=reverse)
start = (page - 1) * page_size
end = start + page_size
paged = result[start:end]
return {
"code": 0,
"data": {
"items": paged,
"total": total,
"page": page,
"page_size": page_size,
"pages": (total + page_size - 1) // page_size if total else 0,
}
}
# ============================================================
# 获取单个技能详情
# ============================================================
@router.get("/{skill_id}")
async def get_skill(
skill_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取技能详情(包含 SKILL.md 内容)"""
skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first()
if not skill:
raise HTTPException(status_code=404, detail="技能不存在")
result = skill_to_dict(skill, include_md=True)
# 获取所属用户信息
user = db.query(User).filter(User.id == skill.user_id).first()
if user:
result["owner_username"] = user.username
result["owner_email"] = user.email
# 获取发布记录
pub_records = db.query(SkillPublishRecord).filter(
SkillPublishRecord.skill_id == skill_id
).order_by(desc(SkillPublishRecord.created_at)).limit(10).all()
result["publish_records"] = [{
"id": p.id,
"price": float(p.price) if p.price else 0,
"market_description": p.market_description,
"status": p.status,
"published_at": p.published_at.isoformat() if p.published_at else None,
"created_at": p.created_at.isoformat() if p.created_at else None,
} for p in pub_records]
# 获取最新版本号
latest_version = db.query(func.max(SkillVersion.version)).filter(
SkillVersion.skill_id == skill_id
).scalar()
result["latest_version"] = latest_version or skill.version
return {"code": 0, "data": result}
# ============================================================
# 创建技能
# ============================================================
class CreateSkillRequest:
def __init__(self, skill_name: str = None, description: str = None,
tags: List[str] = None, category: str = None,
skill_md_content: str = None, user_id: str = None):
self.skill_name = skill_name
self.description = description
self.tags = tags or []
self.category = category
self.skill_md_content = skill_md_content
self.user_id = user_id
from pydantic import BaseModel, Field
class CreateSkillSchema(BaseModel):
skill_name: str = Field(..., min_length=1, max_length=200, description="技能名称")
user_id: str = Field(..., description="所属用户ID")
description: Optional[str] = Field(None, description="技能描述")
tags: Optional[List[str]] = Field(default_factory=list, description="标签列表")
category: Optional[str] = Field(None, description="技能分类")
skill_md_content: Optional[str] = Field(None, description="SKILL.md 内容")
@router.post("/")
async def create_skill(
req: CreateSkillSchema,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""管理员创建技能"""
logger.info(f"[Admin Skills] {admin_user.username} 创建技能: {req.skill_name}")
# 检查技能名是否已存在
existing = db.query(EvolutionSkill).filter(
EvolutionSkill.user_id == req.user_id,
EvolutionSkill.skill_name == req.skill_name
).first()
if existing:
raise HTTPException(status_code=400, detail="该用户已存在同名技能")
# 验证用户存在
user = db.query(User).filter(User.id == req.user_id).first()
if not user:
raise HTTPException(status_code=400, detail="用户不存在")
# 生成技能ID
skill_id = str(uuid.uuid4())
# 保存 SKILL.md 文件
skill_md_path = None
if req.skill_md_content:
skill_dir = os.path.join(UPLOAD_DIR, "skills", req.user_id)
os.makedirs(skill_dir, exist_ok=True)
safe_name = req.skill_name.replace("/", "_").replace("\\", "_")
skill_md_path = os.path.join(skill_dir, f"{safe_name}.md")
try:
with open(skill_md_path, 'w', encoding='utf-8') as f:
f.write(req.skill_md_content)
except Exception as e:
logger.warning(f"保存 SKILL.md 失败: {e}")
# 创建技能记录
skill = EvolutionSkill(
id=skill_id,
user_id=req.user_id,
skill_name=req.skill_name,
description=req.description,
tags=req.tags or [],
category=req.category,
state="baby",
use_count=0,
version=1,
skill_md_path=skill_md_path,
git_commit_sha=None,
)
db.add(skill)
# 记录初始版本
if req.skill_md_content:
sv = SkillVersion(
id=str(uuid.uuid4()),
user_id=req.user_id,
skill_name=req.skill_name,
version=1,
skill_md_content=req.skill_md_content,
git_commit_sha=None,
description="初始版本",
)
db.add(sv)
# 记录事件
ev = EvolutionEvent(
id=str(uuid.uuid4()),
user_id=req.user_id,
skill_name=req.skill_name,
event_type="created",
description=f"管理员 {admin_user.username} 创建技能 {req.skill_name}",
version_before=None,
version_after=1,
)
db.add(ev)
db.commit()
db.refresh(skill)
LogService.log_operation(
user_id=admin_user.id,
username=admin_user.username,
action="create_skill",
target="evolution_skills",
detail=f"创建技能 {req.skill_name}(归属用户: {user.username})",
)
return {"code": 0, "data": skill_to_dict(skill, include_md=True)}
# ============================================================
# 更新技能
# ============================================================
class UpdateSkillSchema(BaseModel):
skill_name: Optional[str] = Field(None, min_length=1, max_length=200)
description: Optional[str] = None
tags: Optional[List[str]] = None
category: Optional[str] = None
skill_md_content: Optional[str] = None
@router.put("/{skill_id}")
async def update_skill(
skill_id: str,
req: UpdateSkillSchema,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""更新技能信息"""
skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first()
if not skill:
raise HTTPException(status_code=404, detail="技能不存在")
old_version = skill.version
update_data = req.model_dump(exclude_none=True)
# 处理 SKILL.md 内容
if "skill_md_content" in update_data:
content = update_data.pop("skill_md_content")
if content is not None:
skill_dir = os.path.join(UPLOAD_DIR, "skills", skill.user_id)
os.makedirs(skill_dir, exist_ok=True)
safe_name = skill.skill_name.replace("/", "_").replace("\\", "_")
skill_md_path = os.path.join(skill_dir, f"{safe_name}.md")
try:
with open(skill_md_path, 'w', encoding='utf-8') as f:
f.write(content)
skill.skill_md_path = skill_md_path
except Exception as e:
logger.warning(f"保存 SKILL.md 失败: {e}")
# 记录新版本
new_version = old_version + 1
sv = SkillVersion(
id=str(uuid.uuid4()),
user_id=skill.user_id,
skill_name=skill.skill_name,
version=new_version,
skill_md_content=content,
git_commit_sha=None,
description="管理员更新",
)
db.add(sv)
skill.version = new_version
# 记录事件
ev = EvolutionEvent(
id=str(uuid.uuid4()),
user_id=skill.user_id,
skill_name=skill.skill_name,
event_type="edited",
description=f"管理员 {admin_user.username} 更新技能 {skill.skill_name}",
version_before=old_version,
version_after=new_version,
)
db.add(ev)
# 更新其他字段
for key, value in update_data.items():
if key not in ("skill_md_content",) and hasattr(skill, key):
setattr(skill, key, value)
skill.updated_at = datetime.now()
db.commit()
db.refresh(skill)
LogService.log_operation(
user_id=admin_user.id,
username=admin_user.username,
action="update_skill",
target="evolution_skills",
detail=f"更新技能 {skill.skill_name}(版本 {old_version} → {skill.version})",
)
return {"code": 0, "data": skill_to_dict(skill, include_md=True)}
# ============================================================
# 批量删除技能(用于一次性清空技能市场)
# ============================================================
class BatchDeleteSkillsRequest(BaseModel):
ids: list[str] = [] # 要删除的技能 ID 列表;传空列表则删除所有
@router.post("/batch-delete")
async def batch_delete_skills(
req: BatchDeleteSkillsRequest,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""批量删除技能。ids 为空则删除全部技能。"""
if req.ids:
skills = db.query(EvolutionSkill).filter(EvolutionSkill.id.in_(req.ids)).all()
else:
skills = db.query(EvolutionSkill).all()
deleted_count = 0
for skill in skills:
skill_name = skill.skill_name
# 删除版本历史
db.query(SkillVersion).filter(SkillVersion.skill_name == skill_name).delete()
# 删除发布记录
db.query(SkillPublishRecord).filter(SkillPublishRecord.skill_id == skill.id).delete()
# 删除团队共享
db.query(TeamSharedSkill).filter(TeamSharedSkill.skill_id == skill.id).delete()
# 同步清理市场应用表中的技能
market_app = db.query(MarketApp).filter(
MarketApp.type == "skill",
MarketApp.name == skill_name,
).first()
if market_app:
# 清理关联的安装和购买记录
db.query(AppInstallation).filter(AppInstallation.app_id == market_app.id).delete()
db.query(AppPurchase).filter(AppPurchase.app_id == market_app.id).delete()
db.query(AppReview).filter(AppReview.app_id == market_app.id).delete()
db.delete(market_app)
db.delete(skill)
deleted_count += 1
db.commit()
return {"code": 0, "data": {"deleted": deleted_count}, "message": f"已删除 {deleted_count} 个技能"}
# ============================================================
# 删除技能(单个)
# ============================================================
@router.delete("/{skill_id}")
async def delete_skill(
skill_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""删除技能"""
skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first()
if not skill:
raise HTTPException(status_code=404, detail="技能不存在")
skill_name = skill.skill_name
user_id = skill.user_id
# 记录事件
ev = EvolutionEvent(
id=str(uuid.uuid4()),
user_id=user_id,
skill_name=skill_name,
event_type="deleted",
description=f"管理员 {admin_user.username} 删除技能 {skill_name}",
)
db.add(ev)
# 删除版本历史
db.query(SkillVersion).filter(SkillVersion.skill_name == skill_name).delete()
# 删除发布记录
db.query(SkillPublishRecord).filter(SkillPublishRecord.skill_id == skill_id).delete()
# 删除团队共享记录
db.query(TeamSharedSkill).filter(TeamSharedSkill.skill_id == skill_id).delete()
db.delete(skill)
db.commit()
LogService.log_operation(
user_id=admin_user.id,
username=admin_user.username,
action="delete_skill",
target="evolution_skills",
detail=f"删除技能 {skill_name}",
)
return {"code": 0, "message": f"技能 {skill_name} 已删除"}
# ============================================================
# 修改技能状态
# ============================================================
class ChangeStateSchema(BaseModel):
state: str = Field(..., description="目标状态: baby/main/staging/frozen")
@router.put("/{skill_id}/state")
async def change_skill_state(
skill_id: str,
req: ChangeStateSchema,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""修改技能状态"""
if req.state not in SKILL_STATES:
raise HTTPException(status_code=400, detail=f"无效状态,可选值: {list(SKILL_STATES.keys())}")
skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first()
if not skill:
raise HTTPException(status_code=404, detail="技能不存在")
old_state = skill.state
skill.state = req.state
skill.updated_at = datetime.now()
# 记录状态变更事件
ev_type = "promoted" if req.state in ("main", "staging") else "frozen" if req.state == "frozen" else "unfrozen"
ev = EvolutionEvent(
id=str(uuid.uuid4()),
user_id=skill.user_id,
skill_name=skill.skill_name,
event_type=ev_type,
description=f"管理员 {admin_user.username} 将技能状态从 {old_state} 变更为 {req.state}",
)
db.add(ev)
db.commit()
db.refresh(skill)
LogService.log_operation(
user_id=admin_user.id,
username=admin_user.username,
action="change_skill_state",
target="evolution_skills",
detail=f"修改技能 {skill.skill_name} 状态: {old_state} → {req.state}",
)
return {"code": 0, "data": skill_to_dict(skill)}
# ============================================================
# 获取版本历史
# ============================================================
@router.get("/{skill_id}/versions")
async def get_skill_versions(
skill_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取技能版本历史"""
skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first()
if not skill:
raise HTTPException(status_code=404, detail="技能不存在")
versions = db.query(SkillVersion).filter(
SkillVersion.skill_name == skill.skill_name,
SkillVersion.user_id == skill.user_id,
).order_by(desc(SkillVersion.version)).all()
return {
"code": 0,
"data": {
"skill_id": skill.id,
"skill_name": skill.skill_name,
"current_version": skill.version,
"versions": [version_to_dict(v) for v in versions],
}
}
# ============================================================
# 回滚到指定版本
# ============================================================
class RollbackSchema(BaseModel):
version: int = Field(..., ge=1, description="目标版本号")
@router.post("/{skill_id}/rollback")
async def rollback_skill_version(
skill_id: str,
req: RollbackSchema,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""回滚技能到指定版本"""
skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first()
if not skill:
raise HTTPException(status_code=404, detail="技能不存在")
target_version = db.query(SkillVersion).filter(
SkillVersion.skill_name == skill.skill_name,
SkillVersion.user_id == skill.user_id,
SkillVersion.version == req.version,
).first()
if not target_version:
raise HTTPException(status_code=404, detail=f"版本 {req.version} 不存在")
old_version = skill.version
# 更新 SKILL.md 文件
if target_version.skill_md_content:
if skill.skill_md_path:
try:
with open(skill.skill_md_path, 'w', encoding='utf-8') as f:
f.write(target_version.skill_md_content)
except Exception as e:
logger.warning(f"回滚保存 SKILL.md 失败: {e}")
# 记录新版本(基于目标版本)
new_version = old_version + 1
sv = SkillVersion(
id=str(uuid.uuid4()),
user_id=skill.user_id,
skill_name=skill.skill_name,
version=new_version,
skill_md_content=target_version.skill_md_content,
git_commit_sha=None,
description=f"回滚到版本 {req.version}(管理员操作)",
)
db.add(sv)
skill.version = new_version
skill.updated_at = datetime.now()
ev = EvolutionEvent(
id=str(uuid.uuid4()),
user_id=skill.user_id,
skill_name=skill.skill_name,
event_type="edited",
description=f"管理员 {admin_user.username} 回滚技能 {skill.skill_name} 从版本 {old_version} 到 {req.version}(新版本 {new_version})",
version_before=old_version,
version_after=new_version,
)
db.add(ev)
db.commit()
db.refresh(skill)
LogService.log_operation(
user_id=admin_user.id,
username=admin_user.username,
action="rollback_skill",
target="evolution_skills",
detail=f"回滚技能 {skill.skill_name} 到版本 {req.version}(新版本 {new_version})",
)
return {"code": 0, "data": skill_to_dict(skill, include_md=True)}
# ============================================================
# 发布到应用市场
# ============================================================
class PublishSkillSchema(BaseModel):
price: float = Field(0.0, ge=0, description="定价(0表示免费)")
market_description: str = Field("", description="市场描述")
@router.post("/{skill_id}/publish")
async def publish_skill(
skill_id: str,
req: PublishSkillSchema,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""发布技能到应用市场"""
skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first()
if not skill:
raise HTTPException(status_code=404, detail="技能不存在")
# 检查是否已发布
existing = db.query(SkillPublishRecord).filter(
SkillPublishRecord.skill_id == skill_id,
SkillPublishRecord.status == "published"
).first()
if existing:
# 更新现有发布
existing.price = req.price
existing.market_description = req.market_description
msg = "更新发布信息"
else:
# 创建发布记录
record = SkillPublishRecord(
id=str(uuid.uuid4()),
skill_id=skill_id,
skill_name=skill.skill_name,
user_id=skill.user_id,
price=req.price,
market_description=req.market_description,
status="published",
)
db.add(record)
msg = "发布成功"
db.commit()
LogService.log_operation(
user_id=admin_user.id,
username=admin_user.username,
action="publish_skill",
target="skill_publish_records",
detail=f"{msg}: {skill.skill_name},定价: ¥{req.price}",
)
return {"code": 0, "message": msg, "data": {"skill_id": skill_id, "price": req.price}}
# ============================================================
# 下架技能
# ============================================================
@router.post("/{skill_id}/unpublish")
async def unpublish_skill(
skill_id: str,
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""从应用市场下架技能"""
skill = db.query(EvolutionSkill).filter(EvolutionSkill.id == skill_id).first()
if not skill:
raise HTTPException(status_code=404, detail="技能不存在")
updated = db.query(SkillPublishRecord).filter(
SkillPublishRecord.skill_id == skill_id,
SkillPublishRecord.status == "published"
).update({"status": "unpublished"})
db.commit()
if updated == 0:
raise HTTPException(status_code=400, detail="该技能未发布到市场")
LogService.log_operation(
user_id=admin_user.id,
username=admin_user.username,
action="unpublish_skill",
target="skill_publish_records",
detail=f"下架技能: {skill.skill_name}",
)
return {"code": 0, "message": f"技能 {skill.skill_name} 已下架"}
# ============================================================
# 获取统计数据
# ============================================================
@router.get("/stats/overview")
async def get_skills_stats(
admin_user=Depends(get_current_admin),
db: Session = Depends(get_db),
):
"""获取技能统计概览(EvolutionSkill + MarketApp type=skill 合并统计)"""
# EvolutionSkill 维度
evo_total = db.query(EvolutionSkill).count()
evo_by_state = dict(
db.query(EvolutionSkill.state, func.count(EvolutionSkill.id))
.group_by(EvolutionSkill.state).all()
)
evo_by_category = dict(
db.query(EvolutionSkill.category, func.count(EvolutionSkill.id))
.filter(EvolutionSkill.category.isnot(None))
.group_by(EvolutionSkill.category).all()
)
# MarketApp(type=skill) 维度:状态映射 published→main, 其它→baby
market_total = db.query(MarketApp).filter(MarketApp.type == "skill").count()
market_rows = db.query(MarketApp.status, func.count(MarketApp.id)).filter(
MarketApp.type == "skill"
).group_by(MarketApp.status).all()
market_published = sum(cnt for status, cnt in market_rows if (status or "") == "published")
# 各 category 计数(market 表 category 在 SQL 列有,ORM 无此属性,用 SQL 拿)
market_by_category = {}
try:
market_by_category_rows = db.execute(
text("SELECT category, COUNT(*) FROM market_apps WHERE type='skill' AND category IS NOT NULL GROUP BY category")
).fetchall()
market_by_category = {cat or "other": cnt for cat, cnt in market_by_category_rows}
except Exception:
pass
# 合并
total_skills = evo_total + market_total
by_state = {k: evo_by_state.get(k, 0) for k in SKILL_STATES}
by_state["main"] = by_state.get("main", 0) + market_published
by_state["baby"] = by_state.get("baby", 0) + (market_total - market_published)
by_category = {}
for c in (evo_by_category or {}).items():
by_category[c[0]] = by_category.get(c[0], 0) + c[1]
for c in (market_by_category or {}).items():
by_category[c[0]] = by_category.get(c[0], 0) + c[1]
# 原始发布记录 + market 已发布 = 全部已发布
skill_record_published = db.query(SkillPublishRecord).filter(
SkillPublishRecord.status == "published"
).count()
total_published = skill_record_published + market_published
total_versions = db.query(SkillVersion).count()
return {
"code": 0,
"data": {
"total_skills": total_skills,
"by_state": by_state,
"by_category": by_category,
"total_published": total_published,
"total_versions": total_versions,
}
}