fix(aihr): validate assigned practice starts

This commit is contained in:
2026-07-14 11:27:12 +08:00
parent 347d7b5d5d
commit 516bfabee3
3 changed files with 50 additions and 1 deletions
@@ -473,7 +473,7 @@ public class AihrPracticeSeedService {
}
public StartResponse start(StartRequest request) {
ScenarioSeed scenario = resolveScenario(request == null ? null : request.scenarioId(), null);
ScenarioSeed scenario = resolveScenario(resolveStartScenarioId(request), null);
RoundSeed firstRound = scenario.rounds().get(0);
String sessionId = "seed-" + scenario.id() + "-" + System.currentTimeMillis();
String trainee = resolveTrainee(request, scenario);
@@ -2049,6 +2049,40 @@ public class AihrPracticeSeedService {
return request != null && "mobile".equalsIgnoreCase(request.mode());
}
private String resolveStartScenarioId(StartRequest request) {
String requestedScenarioId = request == null ? "" : firstNonBlank(request.scenarioId(), "").trim();
if (!isMobile(request) || request.assignmentId() == null) {
return requestedScenarioId;
}
String extPartyId = resolveExtPartyId(request, "");
if (isBlank(extPartyId)) {
throw new ServiceException("缺少员工身份,无法开始派发训练");
}
ensureAssignmentTable();
List<Map<String, Object>> rows = jdbcTemplate.queryForList("""
SELECT scenario_id
FROM aihr_practice_assignment
WHERE tenant_id = ?
AND id = ?
AND ext_party_id = ?
AND source <> 'daily'
AND status = '待训练'
LIMIT 1
""", tenantId(), request.assignmentId(), extPartyId.trim());
if (rows.isEmpty()) {
throw new ServiceException("训练任务不存在、已完成或无权访问");
}
Object assignedValue = rows.get(0).get("scenario_id");
String assignedScenarioId = assignedValue == null ? "" : String.valueOf(assignedValue).trim();
if (isBlank(assignedScenarioId)) {
throw new ServiceException("训练任务缺少场景,无法开始");
}
if (!isBlank(requestedScenarioId) && !assignedScenarioId.equals(requestedScenarioId)) {
throw new ServiceException("训练任务场景与请求不一致");
}
return assignedScenarioId;
}
private String resolveExtPartyId(StartRequest request, String trainee) {
if (request == null || isBlank(request.extPartyId())) {
return trainee;
@@ -135,6 +135,20 @@ public class AihrPracticeSeedServiceTest {
assertTrue(method.contains("if (updated == 0)"));
}
@Test
public void mobileAssignmentStartValidatesOwnerStatusAndScenario() throws Exception {
String source = Files.readString(Path.of("src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java"));
int methodStart = source.indexOf("private String resolveStartScenarioId");
int methodEnd = source.indexOf("private String resolveExtPartyId", methodStart);
assertTrue(methodStart >= 0 && methodEnd > methodStart);
String method = source.substring(methodStart, methodEnd);
assertTrue(method.contains("AND ext_party_id = ?"));
assertTrue(method.contains("AND source <> 'daily'"));
assertTrue(method.contains("AND status = '待训练'"));
assertTrue(method.contains("训练任务场景与请求不一致"));
}
@Test
public void mobilePracticeSessionRejectsForeignAndUnknownOwner() {
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
+1
View File
@@ -176,6 +176,7 @@
- 2026-07-14 BRD G3 音频读取权限复核:发现移动端音频下载接口对已登录 SYS 用户未校验角色,普通后台账号理论上可枚举 `ossId` 读取训练录音;现复用 `superadmin/hr_operator` 门禁,APP 用户继续按本人/主管项目范围校验,其他身份返回 403。AIHR 全量测试 `70/70` 通过,未修改生产环境。
- 2026-07-14 BRD 4.4 每日三题资格补强:每日派题现在在组织快照存在 `hire_date` 列时按“入职日期至今不超过 3 个月”判断,边界按自然月且包含当天;旧 Demo 快照没有该列时继续使用“移动端训练记录少于 3 次”的兼容回退。正式试点仍需在外部组织同步落地并校验 `hire_date`,不能把回退口径当成正式新员工资格。
- 2026-07-14 BRD 4.4 每日三题提交幂等复核:前端虽已禁用已完成题,后端原先仍允许重复 POST 覆盖答案、反馈和音频证据;现要求查询和更新均只接受“当天、本人、`status='待训练'`”的记录,并校验更新行数,已完成或历史日期记录保留原始证据,重复或并发请求按“不存在或已完成”处理。
- 2026-07-14 BRD 4.4 派发任务启动复核:移动端带 `assignmentId` 开始训练时,后端现校验任务属于当前员工、来源不是每日题、状态仍为“待训练”,并校验请求场景与派发场景一致;自由选场景请求不受影响,避免客户端改写 `scenarioId` 后错误完成主管派发任务。
- 2026-07-14 BRD G3 候选资料审核状态复核:候选人资料审核接口此前在空请求体时默认写回“待审核”,可能覆盖已有“已通过/已驳回”状态;现要求审核动作显式提交三态之一,空值和未知状态直接拒绝,避免后台误操作回退审核结论。
- 2026-07-14 BRD 案例署名复核:案例库已有负责人字段,但此前上传、整理和入库记录都硬编码为“培训组”,无法追溯案例贡献者;现复用既有 `owner` 字段保存首次上传身份,整理和入库阶段保留原署名,手机号身份仅显示员工末四位,避免把原始手机号写入案例展示。
- 2026-07-14 BRD 候选人关联完整性复核:候选人—员工关联接口此前只校验员工主体在职,允许给不存在的任意 `candidateId` 建立入职关联;现要求候选人必须来自内置候选档案、活动面试会话或当前租户已持久化面试记录,保留已有幂等关联和历史数据兼容。