6bb988cc3e
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 路由
489 lines
18 KiB
Python
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}
|