feat(ai): v2 新增 GenerateReport RPC + ReportService

第 9 个 RPC GenerateReport(学情报告生成):data-ana 学情数据 → LLM 生成 → 结构化提取

新增 ReportService 业务编排层 + GenerateReportRequest/GeneratedReport 模型

gRPC servicer + HTTP POST /v1/ai/generate/report(权限 ai:report:generate)

proto_gen 重新生成 + 测试覆盖(servicer/service/HTTP/模型/权限 共 26 用例)

402 测试通过,覆盖率 88.5%
This commit is contained in:
SpecialX
2026-07-14 22:57:57 +08:00
parent 843b370b3d
commit aac26c7c6f
21 changed files with 1954 additions and 278 deletions

View File

@@ -328,6 +328,75 @@ async def test_optimize_expression(client: httpx.AsyncClient) -> None:
assert body["data"]["degraded"] is True
# ---------------------------------------------------------------------------
# Generate report endpoint
# ---------------------------------------------------------------------------
async def test_generate_report_class_summary(client: httpx.AsyncClient) -> None:
"""POST /v1/ai/generate/report with class_summary returns degraded report (no LLM key)."""
resp = await client.post(
"/v1/ai/generate/report",
json={"class_id": "c-1", "report_type": "class_summary"},
)
assert resp.status_code == 200
body = resp.json()
assert body["success"] is True
# 测试环境无 LLM API key → degraded
assert body["data"]["degraded"] is True
assert "id" in body["data"]
async def test_generate_report_student_detail(client: httpx.AsyncClient) -> None:
"""POST /v1/ai/generate/report with student_detail + student_id."""
resp = await client.post(
"/v1/ai/generate/report",
json={
"class_id": "c-1",
"report_type": "student_detail",
"student_id": "s-001",
},
)
assert resp.status_code == 200
body = resp.json()
assert body["success"] is True
assert body["data"]["degraded"] is True
async def test_generate_report_invalid_type(client: httpx.AsyncClient) -> None:
"""POST /v1/ai/generate/report with invalid report_type returns 422."""
resp = await client.post(
"/v1/ai/generate/report",
json={"class_id": "c-1", "report_type": "invalid_type"},
)
assert resp.status_code == 422
async def test_generate_report_missing_class_id(client: httpx.AsyncClient) -> None:
"""POST /v1/ai/generate/report without class_id returns 422."""
resp = await client.post(
"/v1/ai/generate/report",
json={"report_type": "class_summary"},
)
assert resp.status_code == 422
async def test_generate_report_permission_denied(prod_client: httpx.AsyncClient) -> None:
"""Student role attempting to generate report returns 403."""
resp = await prod_client.post(
"/v1/ai/generate/report",
json={"class_id": "c-1", "report_type": "class_summary"},
headers={
"X-User-Id": "student-1",
"X-User-Role": "student",
},
)
assert resp.status_code == 403
body = resp.json()
assert body["success"] is False
assert body["error"]["code"] == "AI_FORBIDDEN"
# ---------------------------------------------------------------------------
# Lesson plan endpoints
# ---------------------------------------------------------------------------