fix(aihr): audit candidate material reviews
This commit is contained in:
+3
-1
@@ -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() {
|
||||
|
||||
+49
-7
@@ -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) {
|
||||
|
||||
+15
@@ -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`),
|
||||
|
||||
@@ -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`,管理端审核表同步展示。
|
||||
|
||||
@@ -99,6 +99,8 @@ export type CandidateMaterialReview = {
|
||||
materialType: string;
|
||||
fileSize: number;
|
||||
status: string;
|
||||
reviewer?: string | null;
|
||||
reviewedTime?: string | null;
|
||||
time: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user