feat(aihr): add employee mistake book audit
This commit is contained in:
+6
@@ -13,6 +13,7 @@ import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeTeamResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromotionEvidenceResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.MistakeResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewAdviceRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewDetailResponse;
|
||||
import org.dromara.aihr.domain.vo.AihrMobileDto.HomeResponse;
|
||||
@@ -91,6 +92,11 @@ public class AihrMobileController {
|
||||
return detail == null ? R.fail("训练记录不存在") : R.ok(reviewDetailPayload(detail));
|
||||
}
|
||||
|
||||
@GetMapping("/practice/mistakes")
|
||||
public R<List<MistakeResponse>> practiceMistakes(@RequestParam(required = false) String extPartyId) {
|
||||
return R.ok(mobileSeedService.practiceMistakes(ownMobileExtPartyId(extPartyId)));
|
||||
}
|
||||
|
||||
@GetMapping("/practice/reviews")
|
||||
public R<List<RecordResponse>> practiceReviews(@RequestParam(required = false) String projectCode,
|
||||
@RequestParam(required = false) String extPartyId,
|
||||
|
||||
+14
@@ -142,6 +142,20 @@ public final class AihrPracticeDto {
|
||||
public record RecordResponse(Long id, String sessionId, String time, String trainee, String scene, Integer score, String status, String summary, String extPartyId) {
|
||||
}
|
||||
|
||||
public record MistakeResponse(
|
||||
Long id,
|
||||
String sessionId,
|
||||
String scenarioId,
|
||||
String scenarioName,
|
||||
Integer score,
|
||||
String cause,
|
||||
Long retryAssignmentId,
|
||||
String retryStatus,
|
||||
String time,
|
||||
String summary
|
||||
) {
|
||||
}
|
||||
|
||||
public record DialogueResponse(String role, String label, String text, String audioUrl, Long audioOssId, Integer emotion, Integer trust, Boolean redFlag, String coachHint) {
|
||||
public DialogueResponse(String role, String label, String text) {
|
||||
this(role, label, text, "", null, null, null, null, "");
|
||||
|
||||
+5
@@ -9,6 +9,7 @@ import org.dromara.aihr.domain.AihrPracticeDto.PracticeAssignmentResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PracticeTeamResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromotionEvidenceResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.MistakeResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewAdviceRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewDetailResponse;
|
||||
import org.dromara.aihr.domain.vo.AihrMobileDto.HomeResponse;
|
||||
@@ -49,6 +50,10 @@ public class AihrMobileSeedService {
|
||||
return practiceSeedService.mobileHistory(extPartyId, 5);
|
||||
}
|
||||
|
||||
public List<MistakeResponse> practiceMistakes(String extPartyId) {
|
||||
return practiceSeedService.mobileMistakes(extPartyId, 20);
|
||||
}
|
||||
|
||||
public List<RecordResponse> practiceReviews() {
|
||||
return practiceSeedService.pendingReviewRecords(5);
|
||||
}
|
||||
|
||||
+60
@@ -25,6 +25,7 @@ import org.dromara.aihr.domain.AihrPracticeDto.PromptTemplateRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromptTemplateResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PromotionEvidenceResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.MistakeResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewAdviceRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.ReviewDetailResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RubricDimensionResponse;
|
||||
@@ -722,6 +723,65 @@ public class AihrPracticeSeedService {
|
||||
""", this::mapRecord, TENANT_ID, extPartyId.trim(), safeLimit);
|
||||
}
|
||||
|
||||
public List<MistakeResponse> mobileMistakes(String extPartyId, int limit) {
|
||||
ensurePracticeTable();
|
||||
ensureAssignmentTable();
|
||||
if (isBlank(extPartyId)) {
|
||||
return List.of();
|
||||
}
|
||||
int safeLimit = normalizeLimit(limit);
|
||||
return jdbcTemplate.query("""
|
||||
SELECT s.id,
|
||||
s.session_id,
|
||||
s.scenario_id,
|
||||
s.scenario_name,
|
||||
s.total_score,
|
||||
CASE
|
||||
WHEN s.dialogue_json LIKE '%"redFlag":true%' THEN '红线话术'
|
||||
WHEN COALESCE(s.dim_emotion, 100) < 60 THEN '情绪承接'
|
||||
WHEN COALESCE(s.dim_compliance, 100) < 60 THEN '流程合规'
|
||||
ELSE '综合得分不足'
|
||||
END AS cause,
|
||||
(SELECT a.id
|
||||
FROM aihr_practice_assignment a
|
||||
WHERE a.tenant_id = s.tenant_id
|
||||
AND a.ext_party_id = s.ext_party_id
|
||||
AND a.scenario_id = s.scenario_id
|
||||
AND a.source = 'retry'
|
||||
ORDER BY a.id DESC
|
||||
LIMIT 1) AS retry_assignment_id,
|
||||
COALESCE((SELECT a.status
|
||||
FROM aihr_practice_assignment a
|
||||
WHERE a.tenant_id = s.tenant_id
|
||||
AND a.ext_party_id = s.ext_party_id
|
||||
AND a.scenario_id = s.scenario_id
|
||||
AND a.source = 'retry'
|
||||
ORDER BY a.id DESC
|
||||
LIMIT 1), '未生成') AS retry_status,
|
||||
s.finished_time,
|
||||
s.summary
|
||||
FROM aihr_practice_session s
|
||||
WHERE s.tenant_id = ?
|
||||
AND s.mode = 'mobile'
|
||||
AND s.ext_party_id = ?
|
||||
AND s.finished_time IS NOT NULL
|
||||
AND (s.total_score < 80 OR s.dialogue_json LIKE '%"redFlag":true%')
|
||||
ORDER BY s.finished_time DESC, s.id DESC
|
||||
LIMIT ?
|
||||
""", (rs, rowNum) -> new MistakeResponse(
|
||||
rs.getLong("id"),
|
||||
rs.getString("session_id"),
|
||||
rs.getString("scenario_id"),
|
||||
firstNonBlank(rs.getString("scenario_name"), "场景训练"),
|
||||
rs.getInt("total_score"),
|
||||
firstNonBlank(rs.getString("cause"), "综合得分不足"),
|
||||
rs.getObject("retry_assignment_id") == null ? null : rs.getLong("retry_assignment_id"),
|
||||
firstNonBlank(rs.getString("retry_status"), "未生成"),
|
||||
rs.getTimestamp("finished_time") == null ? "" : rs.getTimestamp("finished_time").toLocalDateTime().format(TIME_FORMATTER),
|
||||
firstNonBlank(rs.getString("summary"), "建议回看训练对话和导师改写")
|
||||
), TENANT_ID, extPartyId.trim(), safeLimit);
|
||||
}
|
||||
|
||||
public PracticeTeamResponse practiceTeamSnapshot(String supervisorExtPartyId, int limit) {
|
||||
TeamScope scope = teamScope(supervisorExtPartyId);
|
||||
int safeLimit = normalizeTeamLimit(limit);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
| 移动端手机号登录 | `GET /resource/sms/code`、`POST /auth/mobile/sms-login` | 已复用 sms4j 阿里云配置 `config1` 和 RuoYi `sms` 授权策略;短信发送成功后才写 Redis 验证码;手机号不存在时自动注册 `app_user`;`aihr.sms.dev-fixed-code` 非空时不真发短信、验证码固定(dev 默认 `123456`,prod profile 代码级强制失效) |
|
||||
| 用户侧三端首页 `mobile-uni` hash 路由;旧 `/h5/user`、`/h5/candidate`、`/h5/supervisor` 兼容重定向 | `GET /api/aihr/mobile/home/{role}` | 已接入员工、候选人、主管首页公开只读 API;移动端本地 fallback 保演示 |
|
||||
| 移动端登录后角色识别 | `GET /api/aihr/mobile/me` | 认证后按手机号匹配组织快照;`position_level` 为“主管/项目经理”时进入主管端,否则进入员工端;接口失败回退员工端 |
|
||||
| 移动端员工训练与主管复盘闭环 | 员工复用 `POST /api/train/practice/start`、`/turn`、`/finish`,查询 `GET /api/aihr/mobile/practice/history`、`/profile`;主管查询 `GET /api/aihr/mobile/practice/team`、`/practice/alerts`、`/practice/reviews`、`/practice/reviews/{id}`,标记 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed`,指派 `POST /api/aihr/mobile/practice/assignments` | 员工端登录后带 `Authorization` 与 `clientid` 调用;`mode=mobile` 完成后写入 `aihr_practice_session`。主管接口以后端当前登录手机号映射在职组织快照,仅允许岗位为“主管/项目经理”的账号,并按租户和项目范围返回真实成员、全状态训练记录及非 daily 专项;普通员工返回“无主管权限”。复盘标记只允许首次 `待复盘 -> 已复盘` 创建后续专项,并发重复提交幂等;可带 `incentivePoint` 写入贡献度 |
|
||||
| 移动端员工训练与主管复盘闭环 | 员工复用 `POST /api/train/practice/start`、`/turn`、`/finish`,查询 `GET /api/aihr/mobile/practice/history`、`/practice/mistakes`、`/profile`;主管查询 `GET /api/aihr/mobile/practice/team`、`/practice/alerts`、`/practice/reviews`、`/practice/reviews/{id}`,标记 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed`,指派 `POST /api/aihr/mobile/practice/assignments` | 员工端登录后带 `Authorization` 与 `clientid` 调用;`mode=mobile` 完成后写入 `aihr_practice_session`。错题本按员工本人聚合低分/红线回合,并关联已有 `retry` assignment,不伪造错题结论。主管接口以后端当前登录手机号映射在职组织快照,仅允许岗位为“主管/项目经理”的账号,并按租户和项目范围返回真实成员、全状态训练记录及非 daily 专项;普通员工返回“无主管权限”。复盘标记只允许首次 `待复盘 -> 已复盘` 创建后续专项,并发重复提交幂等;可带 `incentivePoint` 写入贡献度 |
|
||||
| 移动端候选人闭环 | 页面拆为 `/pages/candidate/index/index`、`/interview/index`、`/materials/index`、`/progress/index`、`/study/index`;面试复用 `POST /api/recruit/interview/start`、`/answer`、`/finish` 和 `GET /records`;资料 `POST/GET /api/aihr/mobile/candidate/materials`;预习 `POST /api/knowledge/search`;HR 审核 `GET /api/aihr/hr/candidate/materials`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 候选人端登录后带 `Authorization` 与 `clientid` 调用;首页只按当前手机号对应的真实面试记录和最新资料状态分流,不读取公开 home seed。APP 候选人身份以后端登录手机号为准,前端 `candidateId/candidateName` 只作非 APP 场景兼容参数;面试拉题/评分走真实模型优先;资料写 `sys_oss`/MinIO 和 `aihr_candidate_material`,HR 审核后进度页同步三态;岗前预习查询正式 SOP,不兜前端示例答案 |
|
||||
|
||||
## 后端落点
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# BRD 功能审查与未完成项记录
|
||||
|
||||
更新时间:2026-07-14
|
||||
|
||||
## 1. 审查口径
|
||||
|
||||
本记录以 [物业AI人力资源系统业务需求文档BRD.md](物业AI人力资源系统业务需求文档BRD.md) 为业务事实源,以 [FIGMA_IMPLEMENTATION_MATRIX.md](FIGMA_IMPLEMENTATION_MATRIX.md) 判断页面与视觉落地,以 [API_INTEGRATION.md](API_INTEGRATION.md) 和真实接口/浏览器结果判断运行闭环。
|
||||
|
||||
`Figma` 的 `[x]` 只表示画板对应页面已实现,不代表正式试点数据、组织权限或线上生产验收已经完成。BRD 的“阶段二个人 AI 助理”和三期运营页面不纳入当前一期修复范围。
|
||||
|
||||
## 2. 已确认完成的当前基线
|
||||
|
||||
| 领域 | 当前证据 | 结论 |
|
||||
|---|---|---|
|
||||
| 员工端今日/练/问/我 | `mobile-uni/src/pages/user/*`;员工首页、训练前预习、独立练习路由已发布 | 当前代码和线上包一致,后续只做回归 |
|
||||
| 主管工作台/复盘/派发 | `mobile-uni/src/pages/supervisor/*`;线上工作台已完成视觉回归 | 页面主链路已在位;正式主管身份仍依赖组织数据 |
|
||||
| 候选人面试与 HR 复核 | `mobile-uni/src/pages/candidate/*`、`frontend/src/views/recruit/interview.vue` | 工程闭环已在位,需继续保留人工复核证据 |
|
||||
| SOP/RAG/资料处理 | `frontend/src/views/knowledge/*`、`backend/.../AihrSopSeedService.java` | 搜索、引用、反馈、异步处理和评审入口已在位 |
|
||||
| 对练/每日题/主管带教 | `backend/.../AihrPracticeSeedService.java`、`mobile-uni/src/pages/user/practice` | 主闭环已在位;低分再练、错题归因仍需更明确的用户可见闭环 |
|
||||
| 案例上传/整理/筛选 | `backend/.../AihrCaseService.java`、`mobile-uni/src/pages/user/cases`、`frontend/src/views/knowledge/cases.vue` | 上传与整理已在位,学习详情尚未形成稳定模型 |
|
||||
|
||||
## 3. 未完成项与优先级
|
||||
|
||||
### P0:必须先解决,否则不能称为正式试点完成
|
||||
|
||||
| 缺口 | BRD依据 | 当前事实 | 下一步 |
|
||||
|---|---|---|---|
|
||||
| 正式主管身份、项目范围和数据权限 | 5.4、L1-B、G3 | 代码已有 `aihr_org_snapshot`、岗位/项目过滤和主管接口,但正式上游快照的手机号关联不足,线上视觉测试不能替代真实主管身份验证 | 用正式组织同步 dry-run 先核对项目、岗位、`ext_party_id`、手机号;只在上游数据合规后执行正式同步 |
|
||||
| M5 正式试点证据 | L1-B/C/D、G1/G3/G4 | 本地严格门禁和导出能力已存在;本地样本不能代表正式窗口,不能用 seed/烟测数据替代 | 固定 1–2 个住宅项目和窗口,按唯一在职身份采集训练、人工校准、SOP 评审、完训率和满意度 |
|
||||
| AI 分数与人工复核边界 | 4.1、4.5、G1/G2 | 面试和对练已有人工复核/校准入口;仍需在正式试点报告中保留“AI 建议、人工可否决”的证据 | 导出中保留 AI 分、人工分、复核人、复核时间和最终采用值 |
|
||||
|
||||
### P1:一期 BRD 功能仍未形成完整业务闭环
|
||||
|
||||
| 缺口 | BRD依据 | 当前事实 | 建议实现顺序 |
|
||||
|---|---|---|---|
|
||||
| 岗位-SOP匹配与上岗资格 gating | 3.1、4.2、TechSpec P1 | 当前有岗位选择、SOP 搜索和训练任务,但未发现稳定的 `onboard_task` / `qualification_gate` 业务表与页面闭环 | 先建岗位适用 SOP 只读结果,再补培训任务状态,最后补“未通过不得标记上岗”的资格规则 |
|
||||
| 智能错题本 | 3.1、4.4、TechSpec P1 | 已补 `/api/aihr/mobile/practice/mistakes` 和员工“学”页错题本:按本人聚合低分/红线回合,展示原因、分数、再练状态,并可跳已有 retry assignment;暂未做复杂原因筛选和独立错题表 | 下一步补原因筛选、复盘详情关联和主管侧聚合,仍复用现有 session/assignment/annotation |
|
||||
| 案例学习/视频详情 | 3.1、4.6、矩阵第35行 | `AihrCaseService` 当前用内存 `CaseState` + `aihr_case_record` 保存转写/摘要,没有稳定 media URL、学习点、主管点评和案例详情 API | 先扩持久化契约和权限,再接预渲染样片;没有真实媒体 URL 时不造播放按钮 |
|
||||
| 案例库质量与数量 | L1-B、4.6/4.7 | 上传/整理/筛选路径已有;“至少20条脱敏案例”和每周每项目一条需要正式数据证明 | 增加案例统计/脱敏检查导出,按项目窗口采集,不用演示 seed 冒充 |
|
||||
| RAG 可用率与知识治理 | 4.7、L1-C、G3 | 搜索、引用、反馈、知识缺口和人工评审入口已有;住宅核心流程覆盖率与可用率仍需正式评审数据 | 按住宅核心流程建立评审批次,保留命中片段、评审人、状态、版本和来源 |
|
||||
|
||||
### P2/P3:明确暂缓,不应混入当前修复
|
||||
|
||||
- 个人 AI 助理、个人资料库、网页抓取、引用问答、PPT 生成:BRD 4.8,阶段二。
|
||||
- 开放问题榜、积分/排行榜/荣誉墙、完整七维偏好、独立 AI 教练编排:阶段二/三或后续运营范围。
|
||||
- 师徒关系、拜师池、认证和带徒福利:业务已取消,继续保持不实现。
|
||||
|
||||
## 4. 首批执行顺序
|
||||
|
||||
1. 先完成正式组织快照 dry-run 与项目范围核对;不修改正式数据,不用手机号猜角色。
|
||||
2. 在本地/测试环境实现“错题本只读聚合”,复用现有训练记录和 retry assignment,补 API、员工入口和主管查看边界。
|
||||
3. 再实现岗位-SOP匹配与培训任务状态;资格 gating 放在有真实资格规则后接入,避免先做一个无法运营的开关。
|
||||
4. 单独设计案例详情数据契约,确定媒体来源、样片、学习点和主管点评后再开发详情页。
|
||||
5. 用正式试点窗口跑 BRD L1/M5,而不是继续增加视觉页面。
|
||||
|
||||
## 5. 每批完成定义
|
||||
|
||||
- 有明确 BRD 条款、代码路径和数据证据。
|
||||
- 读写权限按员工本人、主管项目范围、HR/运营角色区分。
|
||||
- 真实接口或浏览器验证通过;不能验证的外部数据依赖必须标明。
|
||||
- 不用 seed、烟测样本或固定演示账号冒充正式试点达标。
|
||||
- 发布前同步矩阵、API 文档、验收脚本和本记录。
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
| 优先级 | 项目 | 需要什么才能继续 |
|
||||
|---|---|---|
|
||||
| P0 | 员工今日线上视觉 | 发布当前 `mobile-uni` 静态包并做线上浏览器回归;本地源码不再继续堆视觉代码。 |
|
||||
| P0 | 线上包与角色路由 | 员工今日、训练前预习和主管工作台当前包已发布并完成浏览器回归;后续只在源码变更后回归,不再把旧线上包当作当前缺口。主管正式角色仍需正式组织数据验证。 |
|
||||
| P1 | 案例学习/视频详情 | 先补真实案例详情模型:`caseId`、媒体 URL、学习点、主管点评、相关训练场景;字段和 API 确定后再实现页面。 |
|
||||
| P1 | 正式组织数据 | 用正式员工、项目和试点时间窗口验证团队量化、成长证据和 M5 导出;不使用本地 smoke 样本替代。 |
|
||||
| P2 | 三期页面 | 个人 AI 助理、开放问题榜、积分/荣誉、完整七维偏好、独立 AI 教练编排按阶段二/三单独立项。 |
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
| # | 页面/状态 | Figma 位置 | 代码路径 | 当前实现 | 是否修正 |
|
||||
|---:|---|---|---|---|---|
|
||||
| 1 | 员工端:今日 | — | `mobile-uni/src/pages/user/today/index.vue` | [x] 已实现 | 本地源码已对齐;线上待发布,线上仍是旧视觉 |
|
||||
| 1 | 员工端:今日 | — | `mobile-uni/src/pages/user/today/index.vue` | [x] 已实现 | 本地源码已对齐;2026-07-14 已发布并用员工账号完成线上浏览器回归 |
|
||||
| 2 | 员工端:练入口 | — | `mobile-uni/src/pages/user/practice/index.vue` | [x] 已实现 | 已验证场景首屏、最近练过、每日三题和独立练习入口;蓝白视觉已复核 |
|
||||
| 3 | 员工端:问 | — | `mobile-uni/src/pages/user/sop/index.vue` | [x] 已实现 | 已验证搜索、引用、反馈状态和总结卡 |
|
||||
| 4 | 员工端:我 | — | `mobile-uni/src/pages/user/profile/index.vue` | [x] 已实现 | 已验证登录、岗位、成长摘要、训练历史和详情入口 |
|
||||
@@ -127,8 +127,8 @@
|
||||
|
||||
## 当前优先级
|
||||
|
||||
1. **P0:主管角色路由**。本地页面存在,但线上主管账号仍可能进入员工首页。
|
||||
2. **P0:线上包追平当前源码**。员工首页、训练页和登录页存在明显视觉版本滞后。
|
||||
3. **P1:按本矩阵完成阶段一验收**。重点是员工训练、主管复盘、候选人面试、知识库和案例闭环。
|
||||
4. **P2:阶段二再做个人 AI 助理和开放问题榜**,先补 API、权限和数据模型,再做页面。
|
||||
5. **P3:量化、偏好、积分和荣誉**,等待稳定训练数据与人工校准后再实现。
|
||||
1. **P0:正式主管角色与项目范围**。线上视觉已发布,但正式主管手机号/外部组织映射和项目级权限仍需用正式组织数据验证。
|
||||
2. **P0:M5 正式试点证据**。本地严格门禁可跑通,但正式窗口内的员工、训练次数、校准、SOP 评审和完训率仍需真实数据。
|
||||
3. **P1:按 BRD 补齐一期业务缺口**。优先是岗位-SOP匹配与上岗资格、错题本可见闭环、案例学习/视频详情的稳定数据模型。
|
||||
4. **P2:案例详情数据模型**。先补 `caseId`、媒体/样片、学习点、主管点评等稳定契约,再实现学习页,不用假视频占位。
|
||||
5. **P3:阶段二/三页面**。个人 AI 助理、开放问题榜、积分/荣誉、完整偏好和独立 AI 教练编排按后续阶段单独立项。
|
||||
|
||||
@@ -68,6 +68,39 @@
|
||||
<view v-else class="empty">暂无今日任务</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loggedIn" class="panel mistake-panel">
|
||||
<view class="panel-head">
|
||||
<view>
|
||||
<text class="panel-title">错题本</text>
|
||||
<text class="panel-caption">从低分和红线回合中自动归集</text>
|
||||
</view>
|
||||
<button class="ghost-button compact-button" :disabled="mistakeLoading" @click="loadMistakes">
|
||||
{{ mistakeLoading ? '加载中' : '刷新' }}
|
||||
</button>
|
||||
</view>
|
||||
<view v-if="mistakes.length" class="mistake-list">
|
||||
<view v-for="item in mistakes" :key="item.id" class="mistake-row">
|
||||
<view class="mistake-copy">
|
||||
<view class="mistake-meta">
|
||||
<text class="mistake-cause">{{ item.cause }}</text>
|
||||
<text class="mistake-score">{{ item.score }} 分</text>
|
||||
</view>
|
||||
<text class="mistake-title">{{ item.scenarioName }}</text>
|
||||
<text class="mistake-desc">{{ item.time }} · {{ item.summary }}</text>
|
||||
<text class="mistake-status">再练状态:{{ item.retryStatus }}</text>
|
||||
</view>
|
||||
<button
|
||||
v-if="item.retryAssignmentId"
|
||||
class="secondary-button compact-button"
|
||||
@click="openMistake(item)"
|
||||
>
|
||||
去再练
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="!mistakeLoading" class="empty">完成训练后,低分和红线回合会自动出现在这里</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loggedIn" class="panel">
|
||||
<view class="panel-head">
|
||||
<text class="panel-title">岗位 SOP</text>
|
||||
@@ -145,9 +178,10 @@ import { onShow } from '@dcloudio/uni-app';
|
||||
import type {
|
||||
KnowledgeSearchResponse,
|
||||
PracticeAssignment,
|
||||
PracticeMistake,
|
||||
PracticePrepCard
|
||||
} from '@/types/api';
|
||||
import { getPracticeAssignments, getPracticePrepCard } from '@/services/practice';
|
||||
import { getPracticeAssignments, getPracticeMistakes, getPracticePrepCard } from '@/services/practice';
|
||||
import { searchKnowledge } from '@/services/knowledge';
|
||||
import { getSelectedPositionProfile, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { buildLearningTopics, selectLearningTopic } from '@/services/learning';
|
||||
@@ -161,10 +195,12 @@ const pendingSopQueryKey = 'aihr_sop_pending_query';
|
||||
const loggedIn = ref(false);
|
||||
const message = ref('');
|
||||
const assignments = ref<PracticeAssignment[]>([]);
|
||||
const mistakes = ref<PracticeMistake[]>([]);
|
||||
const prepCard = ref<PracticePrepCard | null>(null);
|
||||
const knowledgeResult = ref<KnowledgeSearchResponse | null>(null);
|
||||
const activeTopic = ref('岗前 SOP');
|
||||
const assignmentLoading = ref(false);
|
||||
const mistakeLoading = ref(false);
|
||||
const prepLoading = ref(false);
|
||||
const knowledgeLoading = ref(false);
|
||||
const positionProfile = ref(getSelectedPositionProfile());
|
||||
@@ -235,6 +271,19 @@ const loadAssignments = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadMistakes = async () => {
|
||||
if (!loggedIn.value) return;
|
||||
mistakeLoading.value = true;
|
||||
try {
|
||||
mistakes.value = await getPracticeMistakes();
|
||||
} catch (error) {
|
||||
mistakes.value = [];
|
||||
handleRequestError(error, '错题本加载失败');
|
||||
} finally {
|
||||
mistakeLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadPrepCard = async () => {
|
||||
if (!loggedIn.value) return;
|
||||
prepLoading.value = true;
|
||||
@@ -267,6 +316,7 @@ const loadPage = async () => {
|
||||
message.value = '';
|
||||
if (!loggedIn.value) {
|
||||
assignments.value = [];
|
||||
mistakes.value = [];
|
||||
prepCard.value = null;
|
||||
knowledgeResult.value = null;
|
||||
return;
|
||||
@@ -275,7 +325,7 @@ const loadPage = async () => {
|
||||
return;
|
||||
}
|
||||
refreshAuthState();
|
||||
await loadAssignments();
|
||||
await Promise.all([loadAssignments(), loadMistakes()]);
|
||||
activeTopic.value = selectLearningTopic(assignments.value, defaultTopic.value);
|
||||
await Promise.all([
|
||||
loadKnowledge(activeTopic.value),
|
||||
@@ -293,6 +343,12 @@ const openAssignment = (item: PracticeAssignment) => {
|
||||
uni.switchTab({ url: '/pages/user/practice/index' });
|
||||
};
|
||||
|
||||
const openMistake = (item: PracticeMistake) => {
|
||||
if (!item.retryAssignmentId) return;
|
||||
uni.setStorageSync(pendingAssignmentKey, String(item.retryAssignmentId));
|
||||
uni.switchTab({ url: '/pages/user/practice/index' });
|
||||
};
|
||||
|
||||
const goPractice = () => {
|
||||
if (firstPracticeAssignment.value) {
|
||||
uni.setStorageSync(pendingAssignmentKey, String(firstPracticeAssignment.value.id));
|
||||
@@ -320,6 +376,72 @@ onShow(() => {
|
||||
padding-bottom: 42px;
|
||||
}
|
||||
|
||||
.panel-caption {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #7b8494;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mistake-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mistake-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
border: 1px solid #fee2e2;
|
||||
border-radius: 12px;
|
||||
background: #fff8f8;
|
||||
}
|
||||
|
||||
.mistake-copy {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mistake-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mistake-cause {
|
||||
color: #bd0010;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mistake-score {
|
||||
color: #7b8494;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mistake-title,
|
||||
.mistake-desc,
|
||||
.mistake-status {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mistake-title {
|
||||
margin-top: 4px;
|
||||
color: #1f2937;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mistake-desc,
|
||||
.mistake-status {
|
||||
margin-top: 4px;
|
||||
color: #7b8494;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
PracticeFinishResponse,
|
||||
PracticePrepCard,
|
||||
PracticeRecord,
|
||||
PracticeMistake,
|
||||
PracticeTeamSnapshot,
|
||||
PracticeStartResponse,
|
||||
PracticeTurnResponse,
|
||||
@@ -88,6 +89,15 @@ export const getPracticeHistoryDetail = (id: number) =>
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
export const getPracticeMistakes = () => {
|
||||
const extPartyId = encodeURIComponent(currentExtPartyId());
|
||||
return apiRequest<PracticeMistake[]>({
|
||||
url: `/api/aihr/mobile/practice/mistakes?extPartyId=${extPartyId}`,
|
||||
method: 'GET',
|
||||
timeout: 15000
|
||||
});
|
||||
};
|
||||
|
||||
export const getCompetencyProfile = () => {
|
||||
const extPartyId = encodeURIComponent(currentExtPartyId());
|
||||
return apiRequest<CompetencyProfile>({
|
||||
|
||||
@@ -193,6 +193,19 @@ export interface PracticeRecord {
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface PracticeMistake {
|
||||
id: number;
|
||||
sessionId: string;
|
||||
scenarioId: string;
|
||||
scenarioName: string;
|
||||
score: number;
|
||||
cause: string;
|
||||
retryAssignmentId?: number | null;
|
||||
retryStatus: string;
|
||||
time: string;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface PracticeTeamMember {
|
||||
extPartyId: string;
|
||||
name: string;
|
||||
|
||||
@@ -135,3 +135,16 @@ test('训练页保留任务卡、对话区、回复区和结果区', async () =>
|
||||
assert.match(source, /reply-card/);
|
||||
assert.match(source, /result-card/);
|
||||
});
|
||||
|
||||
test('学习页展示真实错题本并可进入已有再练任务', async () => {
|
||||
const [source, practiceService] = await Promise.all([
|
||||
pageSource('../src/pages/user/learning/index.vue'),
|
||||
pageSource('../src/services/practice.ts')
|
||||
]);
|
||||
|
||||
assert.match(source, /错题本/);
|
||||
assert.match(source, /getPracticeMistakes/);
|
||||
assert.match(source, /retryAssignmentId/);
|
||||
assert.match(source, /pendingAssignmentKey/);
|
||||
assert.match(practiceService, /\/api\/aihr\/mobile\/practice\/mistakes/);
|
||||
});
|
||||
|
||||
@@ -330,6 +330,8 @@ contains frontend/src/api/aihr/practice.ts "/api/aihr/prompt-templates"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java "/api/train/practice"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPromptTemplateController.java "/api/aihr/prompt-templates"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "mobileCompletedCount"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "/practice/mistakes"
|
||||
contains mobile-uni/src/pages/user/learning/index.vue "错题本"
|
||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "aihr_practice_session"
|
||||
contains backend/script/sql/aihr_practice_mysql8.sql "aihr_practice_scenario"
|
||||
contains backend/script/sql/aihr_practice_mysql8.sql "aihr_prompt_template"
|
||||
|
||||
Reference in New Issue
Block a user