From 7eb787995d1e87a30909b1c9470d33e6ceccd077 Mon Sep 17 00:00:00 2001 From: let5sne Date: Tue, 14 Jul 2026 06:44:13 +0800 Subject: [PATCH] fix(aihr): scope practice data to tenant --- .../aihr/service/AihrPracticeSeedService.java | 159 +++++++++--------- docs/BRD_IMPLEMENTATION_AUDIT.md | 1 + 2 files changed, 83 insertions(+), 77 deletions(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java index 12888908..32a03213 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java @@ -46,6 +46,7 @@ import org.dromara.aihr.domain.AihrPracticeDto.TurnResponse; import org.dromara.aihr.domain.AihrPracticeDto.TeamMemberResponse; import org.dromara.aihr.service.AihrPracticeLlmService.PracticeTurn; import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.satoken.utils.LoginHelper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -255,7 +256,7 @@ public class AihrPracticeSeedService { AND (? = '' OR position = ?) AND (? = '' OR scenario_type = ?) ORDER BY sort_order ASC, id ASC - """, this::mapScenarioResponse, TENANT_ID, safePosition, safePosition, safeType, safeType); + """, this::mapScenarioResponse, tenantId(), safePosition, safePosition, safeType, safeType); return rows.isEmpty() ? fallbackScenarios() : rows; } catch (DataAccessException e) { return fallbackScenarios(); @@ -274,7 +275,7 @@ public class AihrPracticeSeedService { FROM aihr_practice_scenario WHERE tenant_id = ? AND scenario_code = ? LIMIT 1 - """, this::mapScenarioResponse, TENANT_ID, id.trim()); + """, this::mapScenarioResponse, tenantId(), id.trim()); if (!rows.isEmpty()) { return rows.get(0); } @@ -319,7 +320,7 @@ public class AihrPracticeSeedService { enabled = VALUES(enabled), update_time = VALUES(update_time) """, - TENANT_ID, + tenantId(), code, name, firstNonBlank(request == null ? null : request.position(), "生活顾问"), @@ -355,7 +356,7 @@ public class AihrPracticeSeedService { """, request == null || request.enabled() == null || request.enabled(), Timestamp.valueOf(LocalDateTime.now()), - TENANT_ID, + tenantId(), id.trim() ); return updated > 0; @@ -388,7 +389,7 @@ public class AihrPracticeSeedService { AND enabled = 1 AND (? = '' OR scenario_code = ?) ORDER BY id ASC - """, this::mapRubric, TENANT_ID, safeScenario, safeScenario); + """, this::mapRubric, tenantId(), safeScenario, safeScenario); if (!rows.isEmpty()) { return rows; } @@ -417,7 +418,7 @@ public class AihrPracticeSeedService { rs.getBoolean("enabled"), rs.getString("description"), rubricDimensions(rs.getString("rubric_code")) - ), TENANT_ID); + ), tenantId()); } @Transactional(rollbackFor = Exception.class) @@ -437,9 +438,9 @@ public class AihrPracticeSeedService { scenario_code = VALUES(scenario_code), rubric_name = VALUES(rubric_name), version = VALUES(version), enabled = VALUES(enabled), description = VALUES(description), update_time = VALUES(update_time) """, - TENANT_ID, code, scenarioCode, name, version, enabled, + tenantId(), code, scenarioCode, name, version, enabled, request == null ? null : request.description(), now, now); - jdbcTemplate.update("DELETE FROM aihr_practice_rubric_dimension WHERE tenant_id = ? AND rubric_code = ?", TENANT_ID, code); + jdbcTemplate.update("DELETE FROM aihr_practice_rubric_dimension WHERE tenant_id = ? AND rubric_code = ?", tenantId(), code); List dimensions = request == null || request.dimensions() == null ? List.of() : request.dimensions(); int sort = 10; for (RubricDimensionResponse dimension : dimensions) { @@ -451,7 +452,7 @@ public class AihrPracticeSeedService { INSERT INTO aihr_practice_rubric_dimension (tenant_id, rubric_code, dimension_code, dimension_name, weight, description, sort_order, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - """, TENANT_ID, code, dimension.code().trim(), dimension.name().trim(), weight, + """, tenantId(), code, dimension.code().trim(), dimension.name().trim(), weight, dimension.description(), sort, now, now); sort += 10; } @@ -468,7 +469,7 @@ public class AihrPracticeSeedService { UPDATE aihr_practice_rubric SET enabled = ?, update_time = ? WHERE tenant_id = ? AND rubric_code = ? """, request == null || request.enabled() == null || request.enabled(), - Timestamp.valueOf(LocalDateTime.now()), TENANT_ID, code.trim()) > 0; + Timestamp.valueOf(LocalDateTime.now()), tenantId(), code.trim()) > 0; } public StartResponse start(StartRequest request) { @@ -694,7 +695,7 @@ public class AihrPracticeSeedService { FROM aihr_practice_assignment WHERE tenant_id = ? AND ((source <> 'daily' AND status = '待训练') OR (source = 'daily' AND DATE(create_time) = CURRENT_DATE())) - """, TENANT_ID); + """, tenantId()); } public int mobileLowScoreCount() { @@ -703,7 +704,7 @@ public class AihrPracticeSeedService { SELECT COUNT(*) FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND total_score < 80 - """, TENANT_ID); + """, tenantId()); } public int mobileAverageScore() { @@ -712,7 +713,7 @@ public class AihrPracticeSeedService { SELECT COALESCE(AVG(total_score), 0) FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' - """, Double.class, TENANT_ID); + """, Double.class, tenantId()); return value == null ? 0 : Math.round(value.floatValue()); } @@ -726,7 +727,7 @@ public class AihrPracticeSeedService { WHERE tenant_id = ? AND mode = 'mobile' ORDER BY finished_time DESC, id DESC LIMIT ? - """, this::mapRecord, TENANT_ID, safeLimit); + """, this::mapRecord, tenantId(), safeLimit); } return jdbcTemplate.query(""" SELECT id, session_id, finished_time, trainee_name, scenario_name, total_score, status, summary, ext_party_id @@ -734,7 +735,7 @@ public class AihrPracticeSeedService { WHERE tenant_id = ? AND mode = 'mobile' AND ext_party_id = ? ORDER BY finished_time DESC, id DESC LIMIT ? - """, this::mapRecord, TENANT_ID, extPartyId.trim(), safeLimit); + """, this::mapRecord, tenantId(), extPartyId.trim(), safeLimit); } public List mobileMistakes(String extPartyId, int limit) { @@ -793,7 +794,7 @@ public class AihrPracticeSeedService { 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); + ), tenantId(), extPartyId.trim(), safeLimit); } public PracticeTeamResponse practiceTeamSnapshot(String supervisorExtPartyId, int limit) { @@ -820,7 +821,7 @@ public class AihrPracticeSeedService { ? "COALESCE(NULLIF(person_phone, ''), ext_party_id)" : "ext_party_id"; List args = new ArrayList<>(); - args.add(TENANT_ID); + args.add(tenantId()); StringBuilder where = new StringBuilder("WHERE tenant_id = ? AND employment_status = 'active'"); if (scope.scoped()) { where.append(" AND (ext_party_id IN (").append(inClause(scope.extPartyIds().size())).append(")"); @@ -848,14 +849,14 @@ public class AihrPracticeSeedService { ensurePracticeTable(); ensureAssignmentTable(); List args = new ArrayList<>(); - args.add(TENANT_ID); + args.add(tenantId()); String sessionScope = ""; String assignmentScope = ""; if (scope.scoped()) { sessionScope = " AND ext_party_id IN (" + inClause(scope.extPartyIds().size()) + ")"; args.addAll(scope.extPartyIds()); } - args.add(TENANT_ID); + args.add(tenantId()); if (scope.scoped()) { assignmentScope = " AND ext_party_id IN (" + inClause(scope.extPartyIds().size()) + ")"; args.addAll(scope.extPartyIds()); @@ -884,7 +885,7 @@ public class AihrPracticeSeedService { private List practiceTeamRecords(TeamScope scope, int limit) { ensurePracticeTable(); List args = new ArrayList<>(); - args.add(TENANT_ID); + args.add(tenantId()); StringBuilder where = new StringBuilder("WHERE tenant_id = ? AND mode = 'mobile'"); if (scope.scoped()) { where.append(" AND ext_party_id IN (").append(inClause(scope.extPartyIds().size())).append(")"); @@ -904,7 +905,7 @@ public class AihrPracticeSeedService { private List practiceTeamAssignments(TeamScope scope, int limit) { ensureAssignmentTable(); List args = new ArrayList<>(); - args.add(TENANT_ID); + args.add(tenantId()); StringBuilder where = new StringBuilder("WHERE tenant_id = ? AND source <> 'daily'"); if (scope.scoped()) { where.append(" AND ext_party_id IN (").append(inClause(scope.extPartyIds().size())).append(")"); @@ -925,7 +926,7 @@ public class AihrPracticeSeedService { private List practiceTeamMistakes(TeamScope scope, int limit) { ensurePracticeTable(); List args = new ArrayList<>(); - args.add(TENANT_ID); + args.add(tenantId()); StringBuilder where = new StringBuilder("WHERE tenant_id = ? AND mode = 'mobile' AND finished_time IS NOT NULL"); if (scope.scoped()) { where.append(" AND ext_party_id IN (").append(inClause(scope.extPartyIds().size())).append(")"); @@ -992,7 +993,7 @@ public class AihrPracticeSeedService { } ensurePracticeTable(); List args = new ArrayList<>(); - args.add(TENANT_ID); + args.add(tenantId()); StringBuilder where = new StringBuilder("WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘'"); String party = isBlank(extPartyId) ? "" : extPartyId.trim(); if (!party.isEmpty()) { @@ -1007,7 +1008,7 @@ public class AihrPracticeSeedService { WHERE tenant_id = ? AND project_code = ? AND employment_status = 'active' ) """); - args.add(TENANT_ID); + args.add(tenantId()); args.add(project); } if (scope.scoped()) { @@ -1067,8 +1068,8 @@ public class AihrPracticeSeedService { rs.getDouble("satisfaction_average"), rs.getInt("calibration_count"), rs.getInt("calibration_matched") - ), TENANT_ID, startTime, endTime); - sessionRows = jdbcTemplate.queryForList(PILOT_SESSION_ROWS_SQL, TENANT_ID, startTime, endTime); + ), tenantId(), startTime, endTime); + sessionRows = jdbcTemplate.queryForList(PILOT_SESSION_ROWS_SQL, tenantId(), startTime, endTime); } catch (DataAccessException e) { throw new ServiceException("正式试点数据不可用,请先完成组织快照和二期数据表初始化"); } @@ -1175,7 +1176,7 @@ public class AihrPracticeSeedService { GROUP BY ext_party_id HAVING COUNT(*) >= 10 ) completed_people - """, TENANT_ID); + """, tenantId()); } private int countPilotPeople() { @@ -1194,7 +1195,7 @@ public class AihrPracticeSeedService { AND status = '待训练' AND (source <> 'daily' OR DATE(create_time) = CURRENT_DATE()) ) pilot_people - """, TENANT_ID, TENANT_ID); + """, tenantId(), tenantId()); } public ReviewDetailResponse reviewDetail(Long id) { @@ -1207,7 +1208,7 @@ public class AihrPracticeSeedService { summary, mentor_rewrite, ai_comment, review_advice, incentive_point, dim_compliance, dim_emotion, dim_communication, dim_marketing, dialogue_json, annotations_json FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND id = ? - """, this::mapReviewDetail, TENANT_ID, id); + """, this::mapReviewDetail, tenantId(), id); return rows.isEmpty() ? null : rows.get(0); } @@ -1232,7 +1233,7 @@ public class AihrPracticeSeedService { JOIN aihr_practice_session s ON s.tenant_id = a.tenant_id AND s.session_id = a.session_id WHERE a.tenant_id = ? AND a.oss_id = ? AND s.mode = 'mobile' - """, (rs, rowNum) -> rs.getString("ext_party_id"), TENANT_ID, ossId); + """, (rs, rowNum) -> rs.getString("ext_party_id"), tenantId(), ossId); String owner = identity.trim(); return sessionParties.stream().anyMatch(party -> owner.equals(party) || inTeamScope(owner, party)); } @@ -1264,7 +1265,7 @@ public class AihrPracticeSeedService { incentivePoint(request), incentivePoint(request), Timestamp.valueOf(LocalDateTime.now()), - TENANT_ID, + tenantId(), id ); if (updated == 0) { @@ -1273,7 +1274,7 @@ public class AihrPracticeSeedService { FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND id = ? FOR UPDATE - """, (rs, rowNum) -> rs.getString("status"), TENANT_ID, id); + """, (rs, rowNum) -> rs.getString("status"), tenantId(), id); return !statuses.isEmpty() && "已复盘".equals(statuses.get(0)); } if (request != null && !isBlank(request.nextScenarioId())) { @@ -1317,7 +1318,7 @@ public class AihrPracticeSeedService { SET satisfaction_score = ?, satisfaction_comment = ?, update_time = ? WHERE tenant_id = ? AND mode = 'mobile' AND session_id = ? AND ext_party_id = ? AND finished_time IS NOT NULL """, - request.score(), comment, submittedAt, TENANT_ID, request.sessionId().trim(), extPartyId.trim()); + request.score(), comment, submittedAt, tenantId(), request.sessionId().trim(), extPartyId.trim()); if (updated == 0) { return null; } @@ -1338,7 +1339,7 @@ public class AihrPracticeSeedService { rs.getString("scenario_id"), rs.getString("dialogue_json"), rs.getString("annotations_json") - ), TENANT_ID, sessionId.trim()); + ), tenantId(), sessionId.trim()); if (rows.isEmpty()) { return List.of(); } @@ -1352,7 +1353,7 @@ public class AihrPracticeSeedService { UPDATE aihr_practice_session SET annotations_json = ?, update_time = ? WHERE tenant_id = ? AND session_id = ? - """, writeJson(annotations), Timestamp.valueOf(LocalDateTime.now()), TENANT_ID, sessionId.trim()); + """, writeJson(annotations), Timestamp.valueOf(LocalDateTime.now()), tenantId(), sessionId.trim()); replaceAnnotationRows(sessionId.trim(), annotations); return annotations; } @@ -1374,7 +1375,7 @@ public class AihrPracticeSeedService { rs.getInt("dim_emotion"), rs.getInt("dim_communication"), rs.getInt("dim_marketing") - ), TENANT_ID, sessionId.trim()); + ), tenantId(), sessionId.trim()); if (rows.isEmpty()) { return null; } @@ -1388,7 +1389,7 @@ public class AihrPracticeSeedService { (tenant_id, session_id, reviewer, original_score, corrected_score, corrected_dimensions_json, reason, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?, '已记录', ?, ?) """, - TENANT_ID, + tenantId(), sessionId.trim(), reviewer, score.total(), @@ -1422,7 +1423,7 @@ public class AihrPracticeSeedService { FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND id = ? LIMIT 1 - """, (rs, rowNum) -> rs.getString("ext_party_id"), TENANT_ID, id); + """, (rs, rowNum) -> rs.getString("ext_party_id"), tenantId(), id); return rows.isEmpty() ? "" : rows.get(0); } @@ -1448,7 +1449,7 @@ public class AihrPracticeSeedService { rs.getInt("communication"), rs.getInt("emotion"), rs.getInt("incentive_points") - ), TENANT_ID, party, party); + ), tenantId(), party, party); CompetencySnapshot snapshot = rows.isEmpty() ? new CompetencySnapshot(0, 0, 0, 0, 0, 0, 0) : rows.get(0); int trainingMinutes = snapshot.completed() * 8; int contribution = snapshot.completed() == 0 && snapshot.incentivePoints() == 0 @@ -1546,7 +1547,7 @@ public class AihrPracticeSeedService { SELECT COUNT(*) FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND total_score < 80 - """, TENANT_ID); + """, tenantId()); List result = new ArrayList<>(); if (pending > 0) { result.add(new PracticeAlertResponse("pending_review", "待复盘积压", pending + " 条对练等待主管复盘", "medium", "")); @@ -1598,7 +1599,7 @@ public class AihrPracticeSeedService { create_time DESC, id DESC LIMIT ? - """, this::mapAssignment, TENANT_ID, party, party, normalizeLimit(limit)); + """, this::mapAssignment, tenantId(), party, party, normalizeLimit(limit)); } public List assignments(String extPartyId, String supervisorExtPartyId, int limit) { @@ -1620,7 +1621,7 @@ public class AihrPracticeSeedService { } ensureAssignmentTable(); List args = new ArrayList<>(); - args.add(TENANT_ID); + args.add(tenantId()); args.addAll(scope.extPartyIds()); args.add(normalizeLimit(limit)); return jdbcTemplate.query(""" @@ -1647,7 +1648,7 @@ public class AihrPracticeSeedService { SELECT COUNT(*) FROM aihr_practice_assignment WHERE tenant_id = ? AND ext_party_id = ? AND source = 'daily' AND DATE(create_time) = CURRENT_DATE() - """, TENANT_ID, extPartyId); + """, tenantId(), extPartyId); if (existingToday >= 3) { return; } @@ -1662,7 +1663,7 @@ public class AihrPracticeSeedService { (tenant_id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, 'daily', ?, '待训练', ?, ?) """, - TENANT_ID, + tenantId(), extPartyId, extPartyId.matches("\\d{11}") ? "手机用户" + extPartyId.substring(7) : extPartyId, scenario.id(), @@ -1691,7 +1692,7 @@ public class AihrPracticeSeedService { INSERT INTO aihr_practice_assignment (tenant_id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?, '待训练', ?, ?) - """, TENANT_ID, extPartyId, traineeName, scenario.id(), firstNonBlank(request == null ? null : request.scenarioName(), scenario.name()), source, reason, createdAt, createdAt); + """, tenantId(), extPartyId, traineeName, scenario.id(), firstNonBlank(request == null ? null : request.scenarioName(), scenario.name()), source, reason, createdAt, createdAt); List rows = jdbcTemplate.query(""" SELECT id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, answer_text, answer_audio_url, answer_audio_oss_id, feedback, correct @@ -1699,7 +1700,7 @@ public class AihrPracticeSeedService { WHERE tenant_id = ? AND ext_party_id = ? AND scenario_id = ? AND source = ? AND reason = ? ORDER BY id DESC LIMIT 1 - """, this::mapAssignment, TENANT_ID, extPartyId, scenario.id(), source, reason); + """, this::mapAssignment, tenantId(), extPartyId, scenario.id(), source, reason); return rows.isEmpty() ? new PracticeAssignmentResponse(null, extPartyId, traineeName, scenario.id(), scenario.name(), source, reason, "待训练", now.format(TIME_FORMATTER), null, null, null, null, null, null, null) : rows.get(0); } @@ -1730,7 +1731,7 @@ public class AihrPracticeSeedService { """, Timestamp.valueOf(LocalDateTime.now()), Timestamp.valueOf(LocalDateTime.now()), - TENANT_ID, + tenantId(), assignmentId, activeSession.extPartyId() ); @@ -1752,7 +1753,7 @@ public class AihrPracticeSeedService { WHERE tenant_id = ? AND id = ? AND source = 'daily' AND (? = '' OR ext_party_id = ?) LIMIT 1 - """, TENANT_ID, id, party, party); + """, tenantId(), id, party, party); if (rows.isEmpty()) { return null; } @@ -1781,7 +1782,7 @@ public class AihrPracticeSeedService { score.correct() ? 1 : 0, Timestamp.valueOf(now), Timestamp.valueOf(now), - TENANT_ID, + tenantId(), id ); return new DailyDrillAnswerResponse(id, "已完成", score.correct(), score.score(), score.feedback(), parts.referenceAnswer(), audioUrl, audioOssId); @@ -1800,7 +1801,7 @@ public class AihrPracticeSeedService { AND (? = '' OR code = ?) AND (? = -1 OR enabled = ?) ORDER BY category ASC, code ASC, version DESC, id DESC - """, this::mapPromptTemplate, TENANT_ID, safeCategory, safeCategory, safeCode, safeCode, enabledFilter, enabledFilter); + """, this::mapPromptTemplate, tenantId(), safeCategory, safeCategory, safeCode, safeCode, enabledFilter, enabledFilter); } public PromptTemplateResponse savePromptTemplate(PromptTemplateRequest request) { @@ -1823,7 +1824,7 @@ public class AihrPracticeSeedService { description = VALUES(description), update_time = VALUES(update_time) """, - TENANT_ID, + tenantId(), code, firstNonBlank(request == null ? null : request.name(), code), firstNonBlank(request == null ? null : request.category(), "practice"), @@ -1868,7 +1869,7 @@ public class AihrPracticeSeedService { request == null || request.enabled() == null ? current.enabled() : request.enabled(), request == null || request.description() == null ? current.description() : request.description(), Timestamp.valueOf(now), - TENANT_ID, + tenantId(), id ); List updated = promptTemplateById(id); @@ -1930,7 +1931,7 @@ public class AihrPracticeSeedService { finished_time = VALUES(finished_time), update_time = VALUES(update_time) """, - TENANT_ID, + tenantId(), id, activeSession == null ? record.trainee() : activeSession.extPartyId(), record.trainee(), @@ -1968,7 +1969,7 @@ public class AihrPracticeSeedService { SELECT COUNT(*) FROM aihr_practice_assignment WHERE tenant_id = ? AND ext_party_id = ? AND scenario_id = ? AND source = 'retry' AND status = '待训练' - """, TENANT_ID, extPartyId, scenario.id()); + """, tenantId(), extPartyId, scenario.id()); if (exists > 0) { return; } @@ -1978,7 +1979,7 @@ public class AihrPracticeSeedService { (tenant_id, ext_party_id, trainee_name, scenario_id, scenario_name, source, reason, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, 'retry', ?, '待训练', ?, ?) """, - TENANT_ID, + tenantId(), extPartyId, record.trainee(), scenario.id(), @@ -1996,13 +1997,13 @@ public class AihrPracticeSeedService { SELECT COUNT(*) FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' - """, TENANT_ID); + """, tenantId()); } return count(""" SELECT COUNT(*) FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND ext_party_id = ? - """, TENANT_ID, extPartyId.trim()); + """, tenantId(), extPartyId.trim()); } private int countPendingReview(String extPartyId) { @@ -2012,13 +2013,13 @@ public class AihrPracticeSeedService { SELECT COUNT(*) FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘' - """, TENANT_ID); + """, tenantId()); } return count(""" SELECT COUNT(*) FROM aihr_practice_session WHERE tenant_id = ? AND mode = 'mobile' AND status = '待复盘' AND ext_party_id = ? - """, TENANT_ID, extPartyId.trim()); + """, tenantId(), extPartyId.trim()); } private int countPendingReview(TeamScope scope) { @@ -2026,7 +2027,7 @@ public class AihrPracticeSeedService { return 0; } List args = new ArrayList<>(); - args.add(TENANT_ID); + args.add(tenantId()); args.addAll(scope.extPartyIds()); return count(""" SELECT COUNT(*) @@ -2041,7 +2042,7 @@ public class AihrPracticeSeedService { return 0; } List args = new ArrayList<>(); - args.add(TENANT_ID); + args.add(tenantId()); args.addAll(scope.extPartyIds()); return count(""" SELECT COUNT(*) @@ -2066,7 +2067,7 @@ public class AihrPracticeSeedService { } boolean hasPhone = orgSnapshotColumnExists("person_phone"); List args = new ArrayList<>(); - args.add(TENANT_ID); + args.add(tenantId()); args.add(identity); String identityWhere = "ext_party_id = ?"; if (hasPhone) { @@ -2096,7 +2097,7 @@ public class AihrPracticeSeedService { } boolean hasPhone = orgSnapshotColumnExists("person_phone"); List ownerArgs = new ArrayList<>(); - ownerArgs.add(TENANT_ID); + ownerArgs.add(tenantId()); ownerArgs.add(owner); String ownerWhere = "ext_party_id = ?"; if (hasPhone) { @@ -2122,7 +2123,7 @@ public class AihrPracticeSeedService { FROM aihr_org_snapshot WHERE tenant_id = ? AND project_code = ? AND employment_status = 'active' ORDER BY position_level, person_name, ext_party_id - """, (rs, rowNum) -> rs.getString("ext_party_id"), TENANT_ID, row.projectCode()); + """, (rs, rowNum) -> rs.getString("ext_party_id"), tenantId(), row.projectCode()); if (hasPhone) { ids = new ArrayList<>(new LinkedHashSet<>(ids)); ids.addAll(jdbcTemplate.query(""" @@ -2131,7 +2132,7 @@ public class AihrPracticeSeedService { WHERE tenant_id = ? AND project_code = ? AND employment_status = 'active' AND person_phone IS NOT NULL AND person_phone <> '' ORDER BY position_level, person_name, ext_party_id - """, (rs, rowNum) -> rs.getString("person_phone"), TENANT_ID, row.projectCode())); + """, (rs, rowNum) -> rs.getString("person_phone"), tenantId(), row.projectCode())); ids = new ArrayList<>(new LinkedHashSet<>(ids)); } return TeamScope.scoped(ids); @@ -2175,7 +2176,7 @@ public class AihrPracticeSeedService { FROM aihr_practice_calibration WHERE tenant_id = ? AND original_score IS NOT NULL AND corrected_score IS NOT NULL ORDER BY id DESC - """, TENANT_ID).forEach(row -> { + """, tenantId()).forEach(row -> { String sessionId = String.valueOf(row.get("session_id")); latestBySession.putIfAbsent(sessionId, new CalibrationPair( ((Number) row.get("original_score")).intValue(), @@ -2356,7 +2357,7 @@ public class AihrPracticeSeedService { FROM aihr_prompt_template WHERE tenant_id = ? AND code = ? AND output_format = ? AND version = ? LIMIT 1 - """, this::mapPromptTemplate, TENANT_ID, code, outputFormat, version); + """, this::mapPromptTemplate, tenantId(), code, outputFormat, version); return rows.isEmpty() ? null : rows.get(0); } @@ -2366,7 +2367,7 @@ public class AihrPracticeSeedService { FROM aihr_prompt_template WHERE tenant_id = ? AND id = ? LIMIT 1 - """, this::mapPromptTemplate, TENANT_ID, id); + """, this::mapPromptTemplate, tenantId(), id); } private CalibrationResponse latestCalibration(String sessionId) { @@ -2388,7 +2389,7 @@ public class AihrPracticeSeedService { rs.getString("status"), createTime == null ? "" : createTime.toLocalDateTime().format(TIME_FORMATTER) ); - }, TENANT_ID, sessionId); + }, tenantId(), sessionId); return rows.isEmpty() ? null : rows.get(0); } @@ -2476,7 +2477,7 @@ public class AihrPracticeSeedService { WHERE tenant_id = ? AND scenario_code = ? AND enabled = 1 ORDER BY id ASC LIMIT 1 - """, this::mapRubric, TENANT_ID, scenarioCode); + """, this::mapRubric, tenantId(), scenarioCode); return rows.stream().findFirst(); } catch (DataAccessException e) { return Optional.empty(); @@ -2498,7 +2499,7 @@ public class AihrPracticeSeedService { rs.getString("dimension_name"), rs.getDouble("weight"), rs.getString("description") - ), TENANT_ID, rubricCode); + ), tenantId(), rubricCode); return rows.isEmpty() ? defaultRubricDimensions(false) : rows; } catch (DataAccessException e) { return defaultRubricDimensions(false); @@ -2733,7 +2734,7 @@ public class AihrPracticeSeedService { rs.getString("scenario_id"), rs.getString("dialogue_json"), rs.getString("annotations_json") - ), TENANT_ID, sessionId); + ), tenantId(), sessionId); if (rows.isEmpty()) { return readAnnotations(fallbackJson, List.of(), scenario); } @@ -2750,7 +2751,7 @@ public class AihrPracticeSeedService { ensureAnnotationTable(); String id = sessionId.trim(); LocalDateTime now = LocalDateTime.now(); - jdbcTemplate.update("DELETE FROM aihr_practice_annotation WHERE tenant_id = ? AND session_id = ?", TENANT_ID, id); + jdbcTemplate.update("DELETE FROM aihr_practice_annotation WHERE tenant_id = ? AND session_id = ?", tenantId(), id); for (AnnotationResponse annotation : annotations) { if (annotation == null) { continue; @@ -2760,7 +2761,7 @@ public class AihrPracticeSeedService { (tenant_id, session_id, turn_index, source_text, issue_text, rewrite_text, sop_point, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, - TENANT_ID, + tenantId(), id, annotation.turnIndex(), annotation.sourceText(), @@ -2785,7 +2786,7 @@ public class AihrPracticeSeedService { ensureAudioTable(); String id = sessionId.trim(); LocalDateTime now = LocalDateTime.now(); - jdbcTemplate.update("DELETE FROM aihr_practice_audio WHERE tenant_id = ? AND session_id = ?", TENANT_ID, id); + jdbcTemplate.update("DELETE FROM aihr_practice_audio WHERE tenant_id = ? AND session_id = ?", tenantId(), id); for (int i = 0; i < urls.size(); i++) { String url = urls.get(i); if (isBlank(url)) { @@ -2796,7 +2797,7 @@ public class AihrPracticeSeedService { (tenant_id, session_id, turn_index, role, oss_id, audio_url, create_time, update_time) VALUES (?, ?, ?, 'trainee', ?, ?, ?, ?) """, - TENANT_ID, + tenantId(), id, i + 1, i < ossIds.size() ? ossIds.get(i) : null, @@ -3321,6 +3322,10 @@ public class AihrPracticeSeedService { return Math.max(0, Math.min(100, value)); } + private String tenantId() { + return firstNonBlank(LoginHelper.getTenantId(), TENANT_ID); + } + private String firstNonBlank(String first, String second) { return isBlank(first) ? second : first.trim(); } @@ -3373,7 +3378,7 @@ public class AihrPracticeSeedService { FROM aihr_practice_scenario WHERE tenant_id = ? AND scenario_code = ? AND enabled = 1 LIMIT 1 - """, this::mapScenarioSeed, TENANT_ID, scenarioId.trim()); + """, this::mapScenarioSeed, tenantId(), scenarioId.trim()); return rows.stream().findFirst(); } catch (DataAccessException e) { return Optional.empty(); diff --git a/docs/BRD_IMPLEMENTATION_AUDIT.md b/docs/BRD_IMPLEMENTATION_AUDIT.md index ddf617a2..c03dc265 100644 --- a/docs/BRD_IMPLEMENTATION_AUDIT.md +++ b/docs/BRD_IMPLEMENTATION_AUDIT.md @@ -96,3 +96,4 @@ - 2026-07-14 仪表盘租户边界复核:管理端首页的面试、训练、知识库、案例、候选资料、组织和模型统计改为按当前登录租户查询;组织同步写入和组织快照查询也改为当前登录租户;无登录上下文时保留本地开发默认租户回退。尚未把批量知识处理队列和完整 SOP 服务的全部租户调用统一替换,后续继续按数据敏感度分批审查。 - 2026-07-14 SOP/RAG 租户边界复核:知识库搜索、知识缺口、SOP 问答评审、答案反馈、异步导入任务、文档/片段/向量写入及 Qdrant tenant filter 改为当前登录租户;无登录上下文时回退本地开发租户 `000000`。定向 AIHR 测试 48/48 通过;正式多租户检索隔离仍需在测试环境做跨租户回归,批量上传队列仍待单独审查。 - 2026-07-14 剩余固定租户审查:`AihrPracticeSeedService` 仍有 79 处运行时 `TENANT_ID`,覆盖场景、训练记录、派题、复盘、导出和 Prompt 管理;这些路径可按同步请求上下文分批迁移。`AihrUploadQueueService` 仍有 19 处固定租户引用,但其后台 worker 会脱离登录线程并调用 SOP 加工,不能机械替换为 `LoginHelper`;下一步需让队列条目携带租户、worker 按条目租户加工,并补跨租户领取/隔离测试。 +- 2026-07-14 训练租户边界修复:`AihrPracticeSeedService` 的场景、训练记录、派题、复盘、导出、Prompt、标注和音频明细运行时查询/写入统一使用当前登录租户;无登录上下文回退本地开发租户 `000000`。定向 AIHR 测试 48/48、管理端完整打包均通过。`AihrUploadQueueService` 仍保留固定租户,原因是后台 worker 需要下一批显式携带条目租户后再调用 SOP 加工,避免机械替换造成跨租户队列停摆。