Files
server-core/app/api/routers/rbac_certifications.py
T
Pine 6bb988cc3e refactor: 角色组织账号模型重构 - 阶段一
1. capabilities 写入 JWT,避免每次请求查库
2. 移除 service 账号类型,非 operator/carrier 统一归为 opc_member
3. 前端统一基于 capabilities 判断权限
4. 清理登录接口死代码,恢复账号密码登录
5. 移除 enterprise/provider/government/investor/developer 角色
6. 清理 seed.py 中多余角色和权限定义
7. 重写 rbac_portals.py 只保留 carrier 路由
2026-09-03 00:35:40 +08:00

489 lines
18 KiB
Python

# -*- coding: utf-8 -*-
"""信用认证体系端点:认证类型/我的认证/提交申请/技能测试/运营审核。
覆盖四大认证线:个人认证 / 企业认证 / OPC人才认证(付费) / 技能认证(按需付费)。
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from ..dependencies import get_db
from ...rbac import require_roles, write_audit
from ...infrastructure.repositories import Database, new_id, utcnow_iso
from ...infrastructure.models import Certification, CertificationType, SkillTest
import json
router = APIRouter(tags=["certifications"])
_ANY = ("opc_member", "carrier", "operator")
_OPERATOR = ("operator",)
# ── Pydantic 请求模型 ──────────────────────────────────────────────────────
class CertApplyRequest(BaseModel):
cert_type: str
materials: dict = {}
level: str = ""
class CertResubmitRequest(BaseModel):
materials: dict = {}
class CertReviewRequest(BaseModel):
status: str # approved / rejected / revoked
review_note: str = ""
credit_points: int | None = None
class SkillTestStartRequest(BaseModel):
skill_domain: str
class SkillTestSubmitRequest(BaseModel):
answers: dict = {}
# ── 认证类型配置 ────────────────────────────────────────────────────────────
@router.get("/cert/types", summary="认证类型列表(按大类分组)")
async def list_cert_types(
category: str = "",
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
types = await db.cert_types.list(category=category)
# 按大类分组
grouped: dict[str, list] = {}
for t in types:
grouped.setdefault(t["category"], []).append(t)
return {"items": types, "grouped": grouped}
@router.get("/cert/types/{code}", summary="认证类型详情")
async def get_cert_type(
code: str,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
t = await db.cert_types.get_by_code(code)
if t is None:
raise HTTPException(status_code=404, detail="认证类型不存在")
return t
# ── 我的认证 ────────────────────────────────────────────────────────────────
@router.get("/me/certs", summary="我的认证列表")
async def my_certs(
cert_type: str = "",
status: str = "",
category: str = "",
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
items = await db.certifications.list(
user_id=user["id"], cert_type=cert_type, status=status, category=category
)
# 统计已获得的信用加分
total_credit = sum(i.get("credit_points", 0) for i in items if i.get("status") in ("active", "approved"))
return {"items": items, "total_credit_points": total_credit}
@router.get("/me/certs/{cert_id}", summary="我的认证详情")
async def get_my_cert(
cert_id: str,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
c = await db.certifications.get(cert_id)
if c is None or c.get("user_id") != user["id"]:
raise HTTPException(status_code=404, detail="认证记录不存在")
return c
@router.post("/cert/apply", summary="提交认证申请")
async def apply_cert(
req: CertApplyRequest,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
# 校验认证类型
cert_type_info = await db.cert_types.get_by_code(req.cert_type)
if cert_type_info is None:
raise HTTPException(status_code=400, detail="认证类型不存在")
# 检查是否已有生效中的同类型认证
existing = await db.certifications.get_active(user["id"], req.cert_type)
if existing:
raise HTTPException(status_code=400, detail="该类型认证已生效,无需重复申请")
# 检查是否有待审核的同类型认证
pending = await db.certifications.list(user_id=user["id"], cert_type=req.cert_type, status="pending")
if pending:
raise HTTPException(status_code=400, detail="该类型认证审核中,请耐心等待")
# 自动审核模式:直接通过
now = utcnow_iso()
status = "approved" if cert_type_info.get("review_mode") == "auto" else "pending"
credit_points = cert_type_info.get("credit_points", 0) if status == "approved" else 0
expires_at = ""
if cert_type_info.get("validity_days", 0) > 0 and status == "approved":
from datetime import datetime, timedelta
exp = datetime.now() + timedelta(days=cert_type_info["validity_days"])
expires_at = exp.strftime("%Y-%m-%dT%H:%M:%S")
cert = await db.certifications.create(
user_id=user["id"],
cert_type=req.cert_type,
cert_category=cert_type_info.get("category", "personal"),
level=req.level,
status=status,
materials=req.materials,
credit_points=credit_points,
fee_amount=cert_type_info.get("fee", 0),
submitted_at=now,
approved_at=now if status == "approved" else "",
expires_at=expires_at,
)
await write_audit(db, action="cert.apply", resource="certification", resource_id=cert["id"],
detail=f"type={req.cert_type}, status={status}", user=user, request=request)
return {"ok": True, "item": cert, "auto_approved": status == "approved"}
@router.post("/cert/{cert_id}/resubmit", summary="重新提交认证(驳回后)")
async def resubmit_cert(
cert_id: str,
req: CertResubmitRequest,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
c = await db.certifications.get(cert_id)
if c is None or c.get("user_id") != user["id"]:
raise HTTPException(status_code=404, detail="认证记录不存在")
if c.get("status") not in ("rejected",):
raise HTTPException(status_code=400, detail="仅驳回状态可重新提交")
updated = await db.certifications.update(cert_id, {
"materials": req.materials,
"status": "pending",
"submitted_at": utcnow_iso(),
"review_note": "",
})
await write_audit(db, action="cert.resubmit", resource="certification", resource_id=cert_id,
detail="重新提交", user=user, request=request)
return {"ok": True, "item": updated}
@router.delete("/cert/{cert_id}", summary="撤销认证申请(审核前)")
async def cancel_cert(
cert_id: str,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
c = await db.certifications.get(cert_id)
if c is None or c.get("user_id") != user["id"]:
raise HTTPException(status_code=404, detail="认证记录不存在")
if c.get("status") not in ("pending", "not_started"):
raise HTTPException(status_code=400, detail="仅审核中状态可撤销")
updated = await db.certifications.update(cert_id, {"status": "revoked"})
await write_audit(db, action="cert.cancel", resource="certification", resource_id=cert_id,
detail="用户撤销申请", user=user, request=request)
return {"ok": True, "item": updated}
# ── 技能测试 ────────────────────────────────────────────────────────────────
@router.get("/skill/domains", summary="技能测试领域列表")
async def list_skill_domains(
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
types = await db.cert_types.list(category="skill")
# 附带用户已完成的最高成绩
result = []
for t in types:
domain = t["code"].replace("skill_", "")
best = await db.skill_tests.get_latest_completed(user["id"], domain)
t["my_best_score"] = best.get("score", 0) if best else 0
t["my_best_level"] = best.get("level", "") if best else ""
result.append(t)
return {"items": result}
@router.post("/skill/test/start", summary="开始技能测试(创建测试记录)")
async def start_skill_test(
req: SkillTestStartRequest,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
# 校验领域
cert_type = await db.cert_types.get_by_code(f"skill_{req.skill_domain}")
if cert_type is None:
raise HTTPException(status_code=400, detail="技能领域不存在")
# 检查是否有未完成的测试
existing = await db.skill_tests.list(user_id=user["id"], skill_domain=req.skill_domain, status="in_progress")
if existing:
return {"ok": True, "item": existing[0], "resumed": True}
test = await db.skill_tests.create(
user_id=user["id"],
skill_domain=req.skill_domain,
status="in_progress",
started_at=utcnow_iso(),
)
await write_audit(db, action="skill.test.start", resource="skill_test", resource_id=test["id"],
detail=f"domain={req.skill_domain}", user=user, request=request)
return {"ok": True, "item": test, "resumed": False}
@router.post("/skill/test/{test_id}/submit", summary="提交技能测试")
async def submit_skill_test(
test_id: str,
req: SkillTestSubmitRequest,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
test = await db.skill_tests.get(test_id)
if test is None or test.get("user_id") != user["id"]:
raise HTTPException(status_code=404, detail="测试记录不存在")
if test.get("status") != "in_progress":
raise HTTPException(status_code=400, detail="测试已提交或已过期")
# 简单评分:从 answers 中计算客观题得分(实际应从题库判分)
objective_score = 0
total_objective = 0
for qid, ans in req.answers.items():
if isinstance(ans, dict) and ans.get("type") == "objective":
total_objective += 1
if ans.get("correct"):
objective_score += 1
score = int(objective_score / max(total_objective, 1) * 100) if total_objective > 0 else 0
# 等级判定
if score >= 95:
level = "expert"
elif score >= 85:
level = "advanced"
elif score >= 75:
level = "intermediate"
elif score >= 60:
level = "beginner"
else:
level = ""
now = utcnow_iso()
updated = await db.skill_tests.update(test_id, {
"status": "completed" if level else "failed",
"score": score,
"level": level,
"objective_score": objective_score,
"answers": req.answers,
"submitted_at": now,
"scored_at": now,
})
# 通过则创建认证记录
cert_id = ""
if level:
cert_type = await db.cert_types.get_by_code(f"skill_{test['skill_domain']}")
credit_map = {"beginner": 10, "intermediate": 20, "advanced": 30, "expert": 40}
credit = credit_map.get(level, 0)
from datetime import datetime, timedelta
exp = datetime.now() + timedelta(days=cert_type.get("validity_days", 730) if cert_type else 730)
cert = await db.certifications.create(
user_id=user["id"],
cert_type=f"skill_{test['skill_domain']}",
cert_category="skill",
level=level,
status="active",
materials={"test_id": test_id, "score": score},
credit_points=credit,
submitted_at=now,
approved_at=now,
expires_at=exp.strftime("%Y-%m-%dT%H:%M:%S"),
)
cert_id = cert["id"]
# 回写 certification_id
await db.skill_tests.update(test_id, {"certification_id": cert_id})
await write_audit(db, action="skill.test.submit", resource="skill_test", resource_id=test_id,
detail=f"score={score}, level={level}", user=user, request=request)
return {"ok": True, "item": updated, "certification_id": cert_id}
@router.get("/skill/test/{test_id}/result", summary="技能测试结果")
async def get_skill_test_result(
test_id: str,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
test = await db.skill_tests.get(test_id)
if test is None or test.get("user_id") != user["id"]:
raise HTTPException(status_code=404, detail="测试记录不存在")
return test
@router.get("/skill/my-certs", summary="我的技能认证列表")
async def my_skill_certs(
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
items = await db.certifications.list(user_id=user["id"], category="skill", status="active")
return {"items": items}
# ── 用户公开认证信息(脱敏) ──────────────────────────────────────────────────
@router.get("/users/{user_id}/certs", summary="获取用户公开认证信息(脱敏)")
async def get_user_public_certs(
user_id: str,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_ANY)),
):
items = await db.certifications.list(user_id=user_id, status="active")
# 脱敏:移除 materials 中的敏感信息
public = []
for c in items:
public.append({
"id": c["id"],
"cert_type": c["cert_type"],
"cert_category": c["cert_category"],
"level": c["level"],
"status": c["status"],
"credit_points": c["credit_points"],
"approved_at": c["approved_at"],
"expires_at": c["expires_at"],
})
return {"items": public}
# ── 运营端:认证审核 ─────────────────────────────────────────────────────────
@router.get("/admin/cert/list", summary="认证申请列表(运营端)")
async def admin_list_certs(
status: str = "",
cert_type: str = "",
category: str = "",
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_OPERATOR)),
):
items = await db.certifications.list(
status=status, cert_type=cert_type, category=category
)
return {"items": items}
@router.get("/admin/cert/{cert_id}", summary="认证申请详情(运营端)")
async def admin_get_cert(
cert_id: str,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_OPERATOR)),
):
c = await db.certifications.get(cert_id)
if c is None:
raise HTTPException(status_code=404, detail="认证记录不存在")
return c
@router.post("/admin/cert/{cert_id}/review", summary="审核认证申请(通过/驳回/撤销)")
async def admin_review_cert(
cert_id: str,
req: CertReviewRequest,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_OPERATOR)),
):
c = await db.certifications.get(cert_id)
if c is None:
raise HTTPException(status_code=404, detail="认证记录不存在")
if req.status not in ("approved", "rejected", "revoked", "active"):
raise HTTPException(status_code=400, detail="状态仅支持 approved/rejected/rejected/active")
# 通过时计算信用加分和有效期
credit_points = req.credit_points
expires_at = c.get("expires_at", "")
if req.status in ("approved", "active"):
if credit_points is None:
cert_type_info = await db.cert_types.get_by_code(c["cert_type"])
credit_points = cert_type_info.get("credit_points", 0) if cert_type_info else 0
if not expires_at:
cert_type_info = await db.cert_types.get_by_code(c["cert_type"])
validity = cert_type_info.get("validity_days", 0) if cert_type_info else 0
if validity > 0:
from datetime import datetime, timedelta
exp = datetime.now() + timedelta(days=validity)
expires_at = exp.strftime("%Y-%m-%dT%H:%M:%S")
updated = await db.certifications.set_status(
cert_id, req.status,
reviewer_id=user["id"],
review_note=req.review_note,
credit_points=credit_points,
expires_at=expires_at,
)
await write_audit(db, action="admin.cert.review", resource="certification", resource_id=cert_id,
detail=f"status={req.status}, credit={credit_points}", user=user, request=request)
return {"ok": True, "item": updated}
@router.get("/admin/cert/types", summary="认证类型配置列表(运营端)")
async def admin_list_cert_types(
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_OPERATOR)),
):
items = await db.cert_types.list(active_only=False)
return {"items": items}
@router.post("/admin/cert/types", summary="新增认证类型(运营端)")
async def admin_create_cert_type(
body: dict,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_OPERATOR)),
):
code = body.get("code", "")
if not code:
raise HTTPException(status_code=400, detail="缺少认证类型编码")
existing = await db.cert_types.get_by_code(code)
if existing:
raise HTTPException(status_code=400, detail="认证类型编码已存在")
t = await db.cert_types.create(**body)
await write_audit(db, action="admin.cert.type.create", resource="certification_type",
resource_id=t["id"], detail=code, user=user, request=request)
return {"ok": True, "item": t}
@router.put("/admin/cert/types/{code}", summary="编辑认证类型(运营端)")
async def admin_update_cert_type(
code: str,
body: dict,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(require_roles(*_OPERATOR)),
):
t = await db.cert_types.update(code, body)
if t is None:
raise HTTPException(status_code=404, detail="认证类型不存在")
await write_audit(db, action="admin.cert.type.update", resource="certification_type",
resource_id=code, detail=json.dumps(body, ensure_ascii=False), user=user, request=request)
return {"ok": True, "item": t}