fix(aihr): audit candidate material reviews

This commit is contained in:
2026-07-14 11:00:36 +08:00
parent 4b177c618b
commit 00cb4d48e9
8 changed files with 75 additions and 8 deletions
@@ -64,7 +64,9 @@ public class AihrCandidateMaterialController {
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
@PostMapping("/api/aihr/hr/candidate/materials/{id}/review")
public R<MaterialReviewResponse> review(@PathVariable Long id, @RequestBody ReviewRequest request) {
return R.ok(materialService.review(id, request));
LoginUser loginUser = LoginHelper.getLoginUser();
String operator = loginUser == null ? "unknown" : loginUser.getUsername();
return R.ok(materialService.review(id, request, operator));
}
private static String currentAppUsername() {
@@ -5,6 +5,7 @@ import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.service.ISysOssService;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
@@ -96,7 +97,7 @@ public class AihrCandidateMaterialService {
ensureTable();
String normalizedStatus = clean(status, "");
return jdbcTemplate.query("""
select id, candidate_id, candidate_name, oss_id, file_name, material_type, file_size, status, create_time
select id, candidate_id, candidate_name, oss_id, file_name, material_type, file_size, status, reviewer, reviewed_time, create_time
from aihr_candidate_material
where tenant_id = ? and (? = '' or status = ?)
order by id desc
@@ -110,18 +111,24 @@ public class AihrCandidateMaterialService {
rs.getString("material_type"),
rs.getLong("file_size"),
rs.getString("status"),
rs.getString("reviewer"),
rs.getTimestamp("reviewed_time") == null ? "" : rs.getTimestamp("reviewed_time").toLocalDateTime().format(TIME_FORMAT),
rs.getTimestamp("create_time").toLocalDateTime()
), tenantId(), normalizedStatus, normalizedStatus);
}
public MaterialReviewResponse review(Long id, ReviewRequest request) {
return review(id, request, null);
}
public MaterialReviewResponse review(Long id, ReviewRequest request, String operator) {
ensureTable();
String status = normalizeReviewStatus(request == null ? null : request.status());
int updated = jdbcTemplate.update("""
update aihr_candidate_material
set status = ?, update_time = now()
set status = ?, reviewer = ?, reviewed_time = now(), update_time = now()
where tenant_id = ? and id = ?
""", status, tenantId(), id);
""", status, clean(operator, "人工审核"), tenantId(), id);
if (updated == 0) {
throw new IllegalArgumentException("候选资料不存在");
}
@@ -148,6 +155,8 @@ public class AihrCandidateMaterialService {
`content_type` varchar(100) DEFAULT NULL COMMENT '文件MIME',
`oss_id` bigint NOT NULL COMMENT '对象存储ID',
`status` varchar(20) DEFAULT '待审核' COMMENT '审核状态',
`reviewer` varchar(100) DEFAULT NULL COMMENT '审核人',
`reviewed_time` datetime DEFAULT NULL COMMENT '审核时间',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
@@ -156,13 +165,15 @@ public class AihrCandidateMaterialService {
KEY `idx_aihr_candidate_material_oss` (`oss_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 候选人补充资料';
""");
ensureColumn("reviewer", "ALTER TABLE aihr_candidate_material ADD COLUMN `reviewer` varchar(100) DEFAULT NULL COMMENT '审核人' AFTER `status`");
ensureColumn("reviewed_time", "ALTER TABLE aihr_candidate_material ADD COLUMN `reviewed_time` datetime DEFAULT NULL COMMENT '审核时间' AFTER `reviewer`");
tableReady = true;
}
}
private MaterialReviewResponse findReview(Long id) {
return jdbcTemplate.queryForObject("""
select id, candidate_id, candidate_name, oss_id, file_name, material_type, file_size, status, create_time
select id, candidate_id, candidate_name, oss_id, file_name, material_type, file_size, status, reviewer, reviewed_time, create_time
from aihr_candidate_material
where tenant_id = ? and id = ?
""", (rs, rowNum) -> reviewRow(
@@ -174,6 +185,8 @@ public class AihrCandidateMaterialService {
rs.getString("material_type"),
rs.getLong("file_size"),
rs.getString("status"),
rs.getString("reviewer"),
rs.getTimestamp("reviewed_time") == null ? "" : rs.getTimestamp("reviewed_time").toLocalDateTime().format(TIME_FORMAT),
rs.getTimestamp("create_time").toLocalDateTime()
), tenantId(), id);
}
@@ -183,10 +196,39 @@ public class AihrCandidateMaterialService {
return clean(tenantId, TENANT_ID);
}
private void ensureColumn(String column, String ddl) {
Integer exists = jdbcTemplate.queryForObject("""
select count(*)
from information_schema.columns
where table_schema = database() and table_name = 'aihr_candidate_material' and column_name = ?
""", Integer.class, column);
if (exists != null && exists > 0) {
return;
}
try {
jdbcTemplate.execute(ddl);
} catch (DataAccessException error) {
if (!isDuplicateColumnError(error)) {
throw error;
}
}
}
private static boolean isDuplicateColumnError(DataAccessException error) {
Throwable current = error;
while (current != null) {
if (String.valueOf(current.getMessage()).contains("Duplicate column")) {
return true;
}
current = current.getCause();
}
return false;
}
private static MaterialReviewResponse reviewRow(Long id, String candidateId, String candidateName, Long ossId,
String fileName, String materialType, Long fileSize, String status,
LocalDateTime createTime) {
return new MaterialReviewResponse(id, candidateId, candidateName, ossId, fileName, materialType, fileSize, status, createTime.format(TIME_FORMAT));
String reviewer, String reviewedTime, LocalDateTime createTime) {
return new MaterialReviewResponse(id, candidateId, candidateName, ossId, fileName, materialType, fileSize, status, reviewer, reviewedTime, createTime.format(TIME_FORMAT));
}
private static String materialType(String fileName) {
@@ -239,7 +281,7 @@ public class AihrCandidateMaterialService {
}
public record MaterialReviewResponse(Long id, String candidateId, String candidateName, Long ossId, String fileName,
String materialType, Long fileSize, String status, String time) {
String materialType, Long fileSize, String status, String reviewer, String reviewedTime, String time) {
}
public record ReviewRequest(String status) {
@@ -4,6 +4,9 @@ import org.dromara.common.core.exception.ServiceException;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -44,4 +47,16 @@ class AihrCandidateMaterialServiceTest {
assertThrows(IllegalArgumentException.class, () -> AihrCandidateMaterialService.normalizeReviewStatus(""));
assertThrows(IllegalArgumentException.class, () -> AihrCandidateMaterialService.normalizeReviewStatus("未知状态"));
}
@Test
@Tag("dev")
void reviewUsesAuthenticatedOperatorForAuditTrail() throws Exception {
Path source = Path.of("src/main/java/org/dromara/aihr/controller/AihrCandidateMaterialController.java");
if (!Files.exists(source)) {
source = Path.of("ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCandidateMaterialController.java");
}
String code = Files.readString(source);
assertTrue(code.contains("review(id, request, operator)"));
}
}
@@ -11,6 +11,8 @@ CREATE TABLE IF NOT EXISTS `aihr_candidate_material` (
`content_type` varchar(100) DEFAULT NULL COMMENT '文件MIME',
`oss_id` bigint NOT NULL COMMENT '对象存储ID',
`status` varchar(20) DEFAULT '待审核' COMMENT '审核状态',
`reviewer` varchar(100) DEFAULT NULL COMMENT '审核人',
`reviewed_time` datetime DEFAULT NULL COMMENT '审核时间',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
+1
View File
@@ -182,3 +182,4 @@
- 2026-07-14 BRD 知识答案反馈复核:原先只更新 `review_status`,没有复核人和复核时间;现由服务端记录登录操作人到 `reviewer/reviewed_time`,管理端待复核表同步展示留痕。
- 2026-07-14 BRD 知识反馈归属复核:答案反馈原先信任客户端传入的片段 ID,未校验知识片段是否属于当前租户;现入库前按当前租户校验片段归属并限制单次最多 20 个片段。
- 2026-07-14 BRD 知识反馈关联复核:答案反馈原先直接接受客户端传入的 `reviewId`;现仅保留当前租户存在的 SOP 评审关联,非法或跨租户 ID 归一为空。
- 2026-07-14 BRD 候选资料审核留痕:候选资料审核此前只有状态三态,没有审核人和审核时间;现由服务端从当前 HR 登录态写入 `reviewer/reviewed_time`,管理端审核表同步展示。
+2
View File
@@ -99,6 +99,8 @@ export type CandidateMaterialReview = {
materialType: string;
fileSize: number;
status: string;
reviewer?: string | null;
reviewedTime?: string | null;
time: string;
};
+2
View File
@@ -254,6 +254,8 @@
<el-tag :type="materialTagType(row.status)" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="reviewer" label="审核人" width="120" />
<el-table-column prop="reviewedTime" label="审核时间" width="170" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :disabled="row.status === '已通过'" @click="reviewMaterial(row, '已通过')">通过</el-button>
+1
View File
@@ -359,6 +359,7 @@ contains mobile-uni/src/services/candidate.ts "/api/recruit/interview/start"
contains mobile-uni/src/services/candidate.ts "/api/aihr/mobile/candidate/materials"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCandidateMaterialController.java "候选人登录态无效"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCandidateMaterialController.java "@SaCheckLogin"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCandidateMaterialController.java "review(id, request, operator)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCandidateMaterialService.java "SUPPORTED_MATERIAL_EXTENSIONS"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCandidateMaterialService.java "审核接口必须明确提交"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "SUPPORTED_AUDIO_EXTENSIONS"