# RIASEC 职业兴趣测评 API(功能版本 **V35**,Flyway **V34__riasec_assessment.sql**) 基于 RIASEC 理论的自研题库,仅供职业兴趣探索参考,不构成心理或职业诊断。 - **题库**:72 道(R/I/A/S/E/C 各 12),SELF_AUTHORED,reverseScored 恒为 false - **每次答卷**:后端从六维中各随机抽取 5 道,共 30 道并持久化,刷新页面不重新抽题 - **量表**:LIKERT_5 = 1 完全不感兴趣 … 5 非常感兴趣 - **计分**:rawScore = 5 道之和,maxScore = 25,percentage = rawScore/25×100(HALF_UP, scale=2) - **排序**:percentage DESC,同分按维度字母升序;前三位组成 `primaryCode` - **结果**:仅 RIASEC 六维聚合分 + 排名 + 探索方向,不存储原始答案 - **探索方向**:career-direction-v0.1,仅"可能感兴趣的职业探索方向" - **只读/脱敏**:不写 candidate_dimension_scores;结果/审计不含原始题面、答案、手机号、邮箱、JWT ## 权限 | 接口 | STUDENT | COMPANY | ADMIN | 未登录 | | --- | --- | --- | --- | --- | | 测评类型 GET /assessments/types | 200 | 200 | 200 | 401 | | 题目 GET /assessments/{type}/questions | 200 | 200 | 200 | 401 | | 创建答卷 POST /assessments/attempts | 200 | 403 | 403 | 401 | | 答卷详情/保存/提交/结果 | 仅本人 | 403 | 403 | 401 | | 我的历史 GET /assessments/me | 仅本人 | 403 | 403 | 401 | 身份一律从 JWT 获取,不接受 userId 参数。 ## 错误码(97xx) | code | 含义 | HTTP | | --- | --- | --- | | 9701 | ASSESSMENT_TYPE_NOT_FOUND | 404 | | 9702 | ASSESSMENT_ATTEMPT_NOT_FOUND | 404 | | 9703 | ASSESSMENT_ATTEMPT_INVALID_STATE | 409 | | 9705 | ASSESSMENT_ANSWER_INVALID | 400 | | 9706 | ASSESSMENT_INCOMPLETE | 400 | | 9707 | ASSESSMENT_CONCURRENT_UPDATE | 409 | | 9708 | ASSESSMENT_DUPLICATE_ACTIVE | 409 | | 9709 | ASSESSMENT_IDEMPOTENCY_CONFLICT | 409 | 重复提交不抛错,直接幂等返回成功(9704 预留,未使用)。 ## 接口 ### GET /api/v1/assessments/types 返回全部 ACTIVE 测评定义(typeCode、displayName、description、instructions、scaleType、questionCount=72、attemptQuestionCount=30、minPerDimension=5、questionSetVersion、rulesVersion)。 ### GET /api/v1/assessments/HOLLAND_RIASEC/questions 返回指定测评的 ACTIVE 题目(按 position;类型非 ACTIVE 或不存在 → 404/9701)。对外隐藏 `dimension`、`reverseScored`、`source`、`questionVersion`、`questionCode`,仅返回: ```json { "id": 301, "position": 1, "stem": "我对使用手工工具制作物品感兴趣。", "scaleType": "LIKERT_5" } ``` **不返回 `questionCode`**:题目内部编码(如 R01)前缀携带维度信息,一律不外露,前端不得用于推导 R/I/A/S/E/C 维度;维度归属仅由服务端计分使用。 ### POST /api/v1/assessments/attempts 仅 STUDENT 且类型须为 **ACTIVE**(INACTIVE/不存在 → 404/9701)。请求:`{ "typeCode": "HOLLAND_RIASEC", "idempotencyKey": "uuid" }` 返回 attemptId、status、totalQuestions(30)、answeredCount、completionRate、questionSetVersion、version、本次 30 道题。不返回 samplingSeed。 **抽题严格约束**:六维各恰好 5 道、总数严格 30;任一维度 ACTIVE 题不足 5 道或六维不齐 → 拒绝创建(400),不静默减题。 **单活跃答卷(并发原子)**:同一学生同一类型同时最多一份 DRAFT/IN_PROGRESS。由数据库唯一约束 `uk_assessment_attempts_active` 原子兜底: - 竞争失败的并发创建请求返回 `409/9708`(不得返回 500); - 同一 `idempotencyKey` 的并发重放返回已创建的原 attempt(幂等 `200`,同一 attemptId); - 仅数据库靠唯一约束判定归属,服务端不再复用被污染的 Hibernate Session 查询, 写入与冲突判定分别使用独立事务/`REQUIRES_NEW`,创建外层显式 `READ_COMMITTED` 以读取已提交数据。 - 答完提交(COMPLETED)后释放 `active_key`,允许重新测评。 ### GET /api/v1/assessments/attempts/{attemptId} 仅本人。返回 30 道题(固定 position)、已保存答案、answeredCount、completionRate、version、当前状态。 ### PATCH /api/v1/assessments/attempts/{attemptId}/answers 请求:`{ "version": 2, "answers": [ { "questionId": 301, "value": 4 } ] }` 支持单题/批量;同题重复保存 upsert;**同一请求内同题不得重复出现(重复 → 400/9705)**;value 1-5;题目必须属于本答卷;版本冲突 409/9707;首次保存后状态置 IN_PROGRESS。 ### POST /api/v1/assessments/attempts/{attemptId}/submit - answeredCount < 30 → 400/9706 - 30 题(每维各 5)→ COMPLETED,生成结果,写审计,返回结果 - 重复提交 → 200 返回原结果,不重复生成结果与审计 - 不写入候选人评分表 ### GET /api/v1/assessments/attempts/{attemptId}/result 答卷 COMPLETED 后返回: ```json { "attemptId": 101, "typeCode": "HOLLAND_RIASEC", "status": "COMPLETED", "primaryCode": "SIC", "primaryType": "S", "dimensionScores": [ { "dimension": "S", "score": 88.00, "answeredCount": 5 } ], "typeRanking": ["S", "I", "C", "R", "A", "E"], "answeredCount": 30, "completionRate": 100.00, "questionSetVersion": "riasec-self-v0.2", "rulesVersion": "riasec-scoring-v0.1", "generatedAt": "2026-08-28T16:00:00", "disclaimer": "本结果仅供职业兴趣探索参考,不构成心理或职业诊断。", "explorationDirections": ["教育辅导", "用户运营", "客户成功", "职业咨询"] } ``` 不返回 studentUserId、phone、email、password、原始答案、企业信息、其他用户信息。 **越权:答卷题目、已保存答案与结果均仅本人(STUDENT)可读;COMPANY / ADMIN 越权访问一律 `403/2002`。**(来源:权限矩阵) ### GET /api/v1/assessments/me?page=0&size=20 分页返回当前学生的答卷与结果概要。 ## 数据库(V34__riasec_assessment.sql,新增 6 张表) - `assessment_definitions`:测评定义(HOLLAND_RIASEC) - `assessment_questions`:72 题,`UNIQUE(definition_id, question_code)`、`UNIQUE(definition_id, position)`、`CHECK(dimension IN R/I/A/S/E/C)`、`CHECK(reverse_scored IN 0,1)` - `assessment_attempts`:答卷主记录,`@Version` 乐观锁,`UNIQUE(student_user_id, idempotency_key)`、`UNIQUE(active_key)`(单活跃原子约束,DRAFT/IN_PROGRESS 非空,终态置 NULL)、`CHECK(status IN DRAFT/IN_PROGRESS/COMPLETED/EXPIRED)` - `assessment_attempt_questions`:本次抽中的 30 题,`UNIQUE(attempt_id, question_id)`、`UNIQUE(attempt_id, position)` - `assessment_answers`:答案,`answer_value` 1-5(CHECK),`UNIQUE(attempt_id, question_id)`(API 层字段仍为 `value`) - `assessment_results`:结果,`attempt_id` 唯一(幂等)。 `dimension_scores_text` / `type_ranking_text` 使用 TEXT 保存 JSON 字符串,兼容 MySQL 8 与 H2 MODE=MySQL。答案列使用 `answer_value`(避免 H2 保留字 `value`,该列名在 MySQL 与 H2 MODE=MySQL 下均兼容)。