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)
|
@SaCheckRole(value = {TenantConstants.SUPER_ADMIN_ROLE_KEY, HR_OPERATOR_ROLE}, mode = SaMode.OR)
|
||||||
@PostMapping("/api/aihr/hr/candidate/materials/{id}/review")
|
@PostMapping("/api/aihr/hr/candidate/materials/{id}/review")
|
||||||
public R<MaterialReviewResponse> review(@PathVariable Long id, @RequestBody ReviewRequest request) {
|
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() {
|
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.common.core.exception.ServiceException;
|
||||||
import org.dromara.system.domain.vo.SysOssVo;
|
import org.dromara.system.domain.vo.SysOssVo;
|
||||||
import org.dromara.system.service.ISysOssService;
|
import org.dromara.system.service.ISysOssService;
|
||||||
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
import org.springframework.jdbc.support.GeneratedKeyHolder;
|
||||||
import org.springframework.jdbc.support.KeyHolder;
|
import org.springframework.jdbc.support.KeyHolder;
|
||||||
@@ -96,7 +97,7 @@ public class AihrCandidateMaterialService {
|
|||||||
ensureTable();
|
ensureTable();
|
||||||
String normalizedStatus = clean(status, "");
|
String normalizedStatus = clean(status, "");
|
||||||
return jdbcTemplate.query("""
|
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
|
from aihr_candidate_material
|
||||||
where tenant_id = ? and (? = '' or status = ?)
|
where tenant_id = ? and (? = '' or status = ?)
|
||||||
order by id desc
|
order by id desc
|
||||||
@@ -110,18 +111,24 @@ public class AihrCandidateMaterialService {
|
|||||||
rs.getString("material_type"),
|
rs.getString("material_type"),
|
||||||
rs.getLong("file_size"),
|
rs.getLong("file_size"),
|
||||||
rs.getString("status"),
|
rs.getString("status"),
|
||||||
|
rs.getString("reviewer"),
|
||||||
|
rs.getTimestamp("reviewed_time") == null ? "" : rs.getTimestamp("reviewed_time").toLocalDateTime().format(TIME_FORMAT),
|
||||||
rs.getTimestamp("create_time").toLocalDateTime()
|
rs.getTimestamp("create_time").toLocalDateTime()
|
||||||
), tenantId(), normalizedStatus, normalizedStatus);
|
), tenantId(), normalizedStatus, normalizedStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MaterialReviewResponse review(Long id, ReviewRequest request) {
|
public MaterialReviewResponse review(Long id, ReviewRequest request) {
|
||||||
|
return review(id, request, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MaterialReviewResponse review(Long id, ReviewRequest request, String operator) {
|
||||||
ensureTable();
|
ensureTable();
|
||||||
String status = normalizeReviewStatus(request == null ? null : request.status());
|
String status = normalizeReviewStatus(request == null ? null : request.status());
|
||||||
int updated = jdbcTemplate.update("""
|
int updated = jdbcTemplate.update("""
|
||||||
update aihr_candidate_material
|
update aihr_candidate_material
|
||||||
set status = ?, update_time = now()
|
set status = ?, reviewer = ?, reviewed_time = now(), update_time = now()
|
||||||
where tenant_id = ? and id = ?
|
where tenant_id = ? and id = ?
|
||||||
""", status, tenantId(), id);
|
""", status, clean(operator, "人工审核"), tenantId(), id);
|
||||||
if (updated == 0) {
|
if (updated == 0) {
|
||||||
throw new IllegalArgumentException("候选资料不存在");
|
throw new IllegalArgumentException("候选资料不存在");
|
||||||
}
|
}
|
||||||
@@ -148,6 +155,8 @@ public class AihrCandidateMaterialService {
|
|||||||
`content_type` varchar(100) DEFAULT NULL COMMENT '文件MIME',
|
`content_type` varchar(100) DEFAULT NULL COMMENT '文件MIME',
|
||||||
`oss_id` bigint NOT NULL COMMENT '对象存储ID',
|
`oss_id` bigint NOT NULL COMMENT '对象存储ID',
|
||||||
`status` varchar(20) DEFAULT '待审核' COMMENT '审核状态',
|
`status` varchar(20) DEFAULT '待审核' COMMENT '审核状态',
|
||||||
|
`reviewer` varchar(100) DEFAULT NULL COMMENT '审核人',
|
||||||
|
`reviewed_time` datetime DEFAULT NULL COMMENT '审核时间',
|
||||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
@@ -156,13 +165,15 @@ public class AihrCandidateMaterialService {
|
|||||||
KEY `idx_aihr_candidate_material_oss` (`oss_id`)
|
KEY `idx_aihr_candidate_material_oss` (`oss_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI HR 候选人补充资料';
|
) 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;
|
tableReady = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MaterialReviewResponse findReview(Long id) {
|
private MaterialReviewResponse findReview(Long id) {
|
||||||
return jdbcTemplate.queryForObject("""
|
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
|
from aihr_candidate_material
|
||||||
where tenant_id = ? and id = ?
|
where tenant_id = ? and id = ?
|
||||||
""", (rs, rowNum) -> reviewRow(
|
""", (rs, rowNum) -> reviewRow(
|
||||||
@@ -174,6 +185,8 @@ public class AihrCandidateMaterialService {
|
|||||||
rs.getString("material_type"),
|
rs.getString("material_type"),
|
||||||
rs.getLong("file_size"),
|
rs.getLong("file_size"),
|
||||||
rs.getString("status"),
|
rs.getString("status"),
|
||||||
|
rs.getString("reviewer"),
|
||||||
|
rs.getTimestamp("reviewed_time") == null ? "" : rs.getTimestamp("reviewed_time").toLocalDateTime().format(TIME_FORMAT),
|
||||||
rs.getTimestamp("create_time").toLocalDateTime()
|
rs.getTimestamp("create_time").toLocalDateTime()
|
||||||
), tenantId(), id);
|
), tenantId(), id);
|
||||||
}
|
}
|
||||||
@@ -183,10 +196,39 @@ public class AihrCandidateMaterialService {
|
|||||||
return clean(tenantId, TENANT_ID);
|
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,
|
private static MaterialReviewResponse reviewRow(Long id, String candidateId, String candidateName, Long ossId,
|
||||||
String fileName, String materialType, Long fileSize, String status,
|
String fileName, String materialType, Long fileSize, String status,
|
||||||
LocalDateTime createTime) {
|
String reviewer, String reviewedTime, LocalDateTime createTime) {
|
||||||
return new MaterialReviewResponse(id, candidateId, candidateName, ossId, fileName, materialType, fileSize, status, createTime.format(TIME_FORMAT));
|
return new MaterialReviewResponse(id, candidateId, candidateName, ossId, fileName, materialType, fileSize, status, reviewer, reviewedTime, createTime.format(TIME_FORMAT));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String materialType(String fileName) {
|
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,
|
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) {
|
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.Tag;
|
||||||
import org.junit.jupiter.api.Test;
|
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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
@@ -44,4 +47,16 @@ class AihrCandidateMaterialServiceTest {
|
|||||||
assertThrows(IllegalArgumentException.class, () -> AihrCandidateMaterialService.normalizeReviewStatus(""));
|
assertThrows(IllegalArgumentException.class, () -> AihrCandidateMaterialService.normalizeReviewStatus(""));
|
||||||
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',
|
`content_type` varchar(100) DEFAULT NULL COMMENT '文件MIME',
|
||||||
`oss_id` bigint NOT NULL COMMENT '对象存储ID',
|
`oss_id` bigint NOT NULL COMMENT '对象存储ID',
|
||||||
`status` varchar(20) DEFAULT '待审核' COMMENT '审核状态',
|
`status` varchar(20) DEFAULT '待审核' COMMENT '审核状态',
|
||||||
|
`reviewer` varchar(100) DEFAULT NULL COMMENT '审核人',
|
||||||
|
`reviewed_time` datetime DEFAULT NULL COMMENT '审核时间',
|
||||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
|
|||||||
@@ -182,3 +182,4 @@
|
|||||||
- 2026-07-14 BRD 知识答案反馈复核:原先只更新 `review_status`,没有复核人和复核时间;现由服务端记录登录操作人到 `reviewer/reviewed_time`,管理端待复核表同步展示留痕。
|
- 2026-07-14 BRD 知识答案反馈复核:原先只更新 `review_status`,没有复核人和复核时间;现由服务端记录登录操作人到 `reviewer/reviewed_time`,管理端待复核表同步展示留痕。
|
||||||
- 2026-07-14 BRD 知识反馈归属复核:答案反馈原先信任客户端传入的片段 ID,未校验知识片段是否属于当前租户;现入库前按当前租户校验片段归属并限制单次最多 20 个片段。
|
- 2026-07-14 BRD 知识反馈归属复核:答案反馈原先信任客户端传入的片段 ID,未校验知识片段是否属于当前租户;现入库前按当前租户校验片段归属并限制单次最多 20 个片段。
|
||||||
- 2026-07-14 BRD 知识反馈关联复核:答案反馈原先直接接受客户端传入的 `reviewId`;现仅保留当前租户存在的 SOP 评审关联,非法或跨租户 ID 归一为空。
|
- 2026-07-14 BRD 知识反馈关联复核:答案反馈原先直接接受客户端传入的 `reviewId`;现仅保留当前租户存在的 SOP 评审关联,非法或跨租户 ID 归一为空。
|
||||||
|
- 2026-07-14 BRD 候选资料审核留痕:候选资料审核此前只有状态三态,没有审核人和审核时间;现由服务端从当前 HR 登录态写入 `reviewer/reviewed_time`,管理端审核表同步展示。
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ export type CandidateMaterialReview = {
|
|||||||
materialType: string;
|
materialType: string;
|
||||||
fileSize: number;
|
fileSize: number;
|
||||||
status: string;
|
status: string;
|
||||||
|
reviewer?: string | null;
|
||||||
|
reviewedTime?: string | null;
|
||||||
time: string;
|
time: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -254,6 +254,8 @@
|
|||||||
<el-tag :type="materialTagType(row.status)" size="small">{{ row.status }}</el-tag>
|
<el-tag :type="materialTagType(row.status)" size="small">{{ row.status }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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">
|
<el-table-column label="操作" width="160" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" :disabled="row.status === '已通过'" @click="reviewMaterial(row, '已通过')">通过</el-button>
|
<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 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 "候选人登录态无效"
|
||||||
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 "@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 "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/AihrCandidateMaterialService.java "审核接口必须明确提交"
|
||||||
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "SUPPORTED_AUDIO_EXTENSIONS"
|
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