fix(aihr): tighten org and daily drill gates

This commit is contained in:
2026-07-14 13:38:58 +08:00
parent acc895e240
commit 1730819bfc
9 changed files with 55 additions and 15 deletions
@@ -156,6 +156,9 @@ mail:
--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商 --- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
# https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用 # https://sms4j.com/doc3/ 差异配置文档地址 支持单厂商多配置,可以配置多个同时使用
aihr: aihr:
practice:
# 仅本地演示允许用训练次数代替 hire_date 推断新员工,生产默认关闭
allow-legacy-daily-drill-fallback: true
org-sync: org-sync:
# 仅开发演示保留姓名/部门展示;生产默认关闭,组织同步只保留必要主体与范围字段 # 仅开发演示保留姓名/部门展示;生产默认关闭,组织同步只保留必要主体与范围字段
store-display-fields: true store-display-fields: true
@@ -110,7 +110,7 @@ public class AihrOrgSyncService {
} }
} }
int phoneLinked = (int) rows.stream().filter(row -> !row.personPhone().isBlank()).count(); int phoneLinked = (int) rows.stream().filter(row -> !row.personPhone().isBlank()).count();
int suspectText = (int) rows.stream().filter(AihrOrgSyncService::hasSuspectText).count(); int suspectText = (int) rows.stream().filter(OrgRow::suspectText).count();
int duplicatePartyIds = countDuplicatePartyIds(rows.stream().map(OrgRow::extPartyId).toList()); int duplicatePartyIds = countDuplicatePartyIds(rows.stream().map(OrgRow::extPartyId).toList());
if (!rows.isEmpty() && phoneLinked < rows.size()) { if (!rows.isEmpty() && phoneLinked < rows.size()) {
warnings.add("外部员工快照手机号覆盖 " + phoneLinked + "/" + rows.size() + ",移动端正式登录仅可映射已带手机号人员"); warnings.add("外部员工快照手机号覆盖 " + phoneLinked + "/" + rows.size() + ",移动端正式登录仅可映射已带手机号人员");
@@ -397,6 +397,10 @@ public class AihrOrgSyncService {
String positionName = firstNonBlank(text(employee, "position_name", "positionName", "job_title", "jobTitle", String positionName = firstNonBlank(text(employee, "position_name", "positionName", "job_title", "jobTitle",
"post_name", "postName", "job_name", "jobName", "role_name", "roleName", "position", "title"), "员工"); "post_name", "postName", "job_name", "jobName", "role_name", "roleName", "position", "title"), "员工");
String positionLevel = firstNonBlank(text(employee, "position_level", "positionLevel", "job_level", "jobLevel"), level(positionName)); String positionLevel = firstNonBlank(text(employee, "position_level", "positionLevel", "job_level", "jobLevel"), level(positionName));
boolean suspectText = hasQuestionMark(projectName)
|| hasQuestionMark(deptName)
|| hasQuestionMark(personName)
|| hasQuestionMark(positionName);
return new OrgRow( return new OrgRow(
projectCode, projectCode,
projectName, projectName,
@@ -407,7 +411,8 @@ public class AihrOrgSyncService {
positionName, positionName,
positionLevel(positionLevel, positionName), positionLevel(positionLevel, positionName),
status(text(employee, "status", "employment_status", "employmentStatus")), status(text(employee, "status", "employment_status", "employmentStatus")),
LocalDate.now() LocalDate.now(),
suspectText
); );
} }
@@ -614,13 +619,6 @@ public class AihrOrgSyncService {
return false; return false;
} }
private static boolean hasSuspectText(OrgRow row) {
return hasQuestionMark(row.projectName())
|| hasQuestionMark(row.deptName())
|| hasQuestionMark(row.personName())
|| hasQuestionMark(row.positionName());
}
private static boolean hasQuestionMark(String value) { private static boolean hasQuestionMark(String value) {
return clean(value).contains("?"); return clean(value).contains("?");
} }
@@ -728,7 +726,8 @@ public class AihrOrgSyncService {
String positionName, String positionName,
String positionLevel, String positionLevel,
String employmentStatus, String employmentStatus,
LocalDate snapshotDate LocalDate snapshotDate,
boolean suspectText
) { ) {
Object[] args(String tenantId) { Object[] args(String tenantId) {
return new Object[] { return new Object[] {
@@ -48,6 +48,7 @@ import org.dromara.aihr.service.AihrPracticeLlmService.PracticeTurn;
import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -214,6 +215,8 @@ public class AihrPracticeSeedService {
private final AihrPracticeLlmService practiceLlmService; private final AihrPracticeLlmService practiceLlmService;
private final AihrSopSeedService sopSeedService; private final AihrSopSeedService sopSeedService;
private final TransactionTemplate pilotExportTransaction; private final TransactionTemplate pilotExportTransaction;
@Value("${aihr.practice.allow-legacy-daily-drill-fallback:${AIHR_PRACTICE_ALLOW_LEGACY_DAILY_DRILL_FALLBACK:false}}")
private boolean allowLegacyDailyDrillFallback;
private volatile boolean practiceTableReady; private volatile boolean practiceTableReady;
private volatile boolean assignmentTableReady; private volatile boolean assignmentTableReady;
private volatile boolean scenarioTableReady; private volatile boolean scenarioTableReady;
@@ -1841,9 +1844,14 @@ public class AihrPracticeSeedService {
.map(hireDate -> isWithinNewHireWindow(hireDate, LocalDate.now())) .map(hireDate -> isWithinNewHireWindow(hireDate, LocalDate.now()))
.orElse(false); .orElse(false);
} }
// The legacy Demo snapshot has no hire_date. Keep its deterministic fallback // Formal production data must provide hire_date. The record-count fallback is
// until the external organization sync supplies the formal field. // intentionally opt-in for the local Demo only; it must not infer employment
return countMobileRecords(extPartyId) < 3; // tenure from training activity in a pilot or production tenant.
return isLegacyDailyDrillEligible(allowLegacyDailyDrillFallback, extPartyId, countMobileRecords(extPartyId));
}
static boolean isLegacyDailyDrillEligible(boolean allowFallback, String extPartyId, int mobileRecordCount) {
return allowFallback && extPartyId != null && !extPartyId.isBlank() && mobileRecordCount < 3;
} }
private Optional<LocalDate> findHireDate(String extPartyId) { private Optional<LocalDate> findHireDate(String extPartyId) {
@@ -76,6 +76,22 @@ public class AihrOrgSyncServiceTest {
assertEquals("项目管理", ReflectionTestUtils.getField(demo, "deptName")); assertEquals("项目管理", ReflectionTestUtils.getField(demo, "deptName"));
} }
@Test
public void suspectTextIsCheckedBeforeDisplayFieldsAreMasked() throws Exception {
ObjectMapper mapper = new ObjectMapper();
AihrOrgSyncService service = new AihrOrgSyncService(new ObjectMapper(), mock(JdbcTemplate.class), mock(TransactionTemplate.class));
var employee = mapper.readTree("""
{"id":"EMP-1","name":"测试?员工","department_name":"项目?管理","project_name":"翡翠湾","position_name":"主管","phone":"13900001111"}
""");
ReflectionTestUtils.setField(service, "storeDisplayFields", false);
Object masked = ReflectionTestUtils.invokeMethod(service, "orgRow", employee, Map.of(), Map.of());
assertEquals("", ReflectionTestUtils.getField(masked, "personName"));
assertEquals("", ReflectionTestUtils.getField(masked, "deptName"));
assertEquals(true, ReflectionTestUtils.getField(masked, "suspectText"));
}
@Test @Test
public void snapshotSuspectTextQuerySearchesLiteralQuestionMarks() { public void snapshotSuspectTextQuerySearchesLiteralQuestionMarks() {
SnapshotJdbcTemplate jdbcTemplate = new SnapshotJdbcTemplate(); SnapshotJdbcTemplate jdbcTemplate = new SnapshotJdbcTemplate();
@@ -120,6 +120,14 @@ public class AihrPracticeSeedServiceTest {
assertFalse(AihrPracticeSeedService.isWithinNewHireWindow(null, asOf)); assertFalse(AihrPracticeSeedService.isWithinNewHireWindow(null, asOf));
} }
@Test
public void legacyDailyDrillFallbackOnlyUsesTrainingCountAfterExplicitOptIn() {
assertTrue(AihrPracticeSeedService.isLegacyDailyDrillEligible(true, "EMP-1", 0));
assertFalse(AihrPracticeSeedService.isLegacyDailyDrillEligible(false, "EMP-1", 0));
assertFalse(AihrPracticeSeedService.isLegacyDailyDrillEligible(true, "EMP-1", 3));
assertFalse(AihrPracticeSeedService.isLegacyDailyDrillEligible(true, "", 0));
}
@Test @Test
public void dailyDrillAnswerDoesNotOverwriteCompletedAttempt() throws Exception { public void dailyDrillAnswerDoesNotOverwriteCompletedAttempt() throws Exception {
String source = Files.readString(Path.of("src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java")); String source = Files.readString(Path.of("src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java"));
+1 -1
View File
@@ -18,7 +18,7 @@
| 移动端手机号登录 | `GET /resource/sms/code`、`POST /auth/mobile/sms-login` | 已复用 sms4j 阿里云配置 `config1` 和 RuoYi `sms` 授权策略;短信发送成功后才写 Redis 验证码;手机号不存在时自动注册 `app_user`;`aihr.sms.dev-fixed-code` 非空时不真发短信、验证码固定(dev 默认 `123456`,prod profile 代码级强制失效) | | 移动端手机号登录 | `GET /resource/sms/code`、`POST /auth/mobile/sms-login` | 已复用 sms4j 阿里云配置 `config1` 和 RuoYi `sms` 授权策略;短信发送成功后才写 Redis 验证码;手机号不存在时自动注册 `app_user`;`aihr.sms.dev-fixed-code` 非空时不真发短信、验证码固定(dev 默认 `123456`,prod profile 代码级强制失效) |
| 用户侧三端首页 `mobile-uni` hash 路由;旧 `/h5/user`、`/h5/candidate`、`/h5/supervisor` 兼容重定向 | `GET /api/aihr/mobile/home/{role}` | 未登录请求只返回不读取租户业务统计的公开首屏 seed;已登录移动端请求自动携带 `Authorization/clientid`,才返回员工/主管真实统计;移动端本地 fallback 保演示 | | 用户侧三端首页 `mobile-uni` hash 路由;旧 `/h5/user`、`/h5/candidate`、`/h5/supervisor` 兼容重定向 | `GET /api/aihr/mobile/home/{role}` | 未登录请求只返回不读取租户业务统计的公开首屏 seed;已登录移动端请求自动携带 `Authorization/clientid`,才返回员工/主管真实统计;移动端本地 fallback 保演示 |
| 移动端登录后角色识别 | `GET /api/aihr/mobile/me` | 认证后按手机号匹配组织快照;`position_level` 为“主管/项目经理”时进入主管端,否则进入员工端;接口失败回退员工端 | | 移动端登录后角色识别 | `GET /api/aihr/mobile/me` | 认证后按手机号匹配组织快照;`position_level` 为“主管/项目经理”时进入主管端,否则进入员工端;接口失败回退员工端 |
| 移动端员工训练与主管复盘闭环 | 员工复用 `POST /api/train/practice/start`、`/turn`、`/finish`,查询 `GET /api/aihr/mobile/practice/history`、`/practice/mistakes`、`/profile`;训练完成后提交 `POST /api/aihr/mobile/practice/satisfaction`;主管查询 `GET /api/aihr/mobile/practice/team`、`/practice/alerts`、`/practice/reviews`、`/practice/reviews/{id}`,标记 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed`,指派 `POST /api/aihr/mobile/practice/assignments` | 员工端登录后带 `Authorization` 与 `clientid` 调用;`mode=mobile` 完成后写入 `aihr_practice_session`。满意度接口只接受本人已完成训练的 1-5 分,意见脱敏后落库,未填写不补默认值。错题本按员工本人聚合低分/红线回合,并关联已有 `retry` assignment,不伪造错题结论。主管 `/practice/team` 在同一项目权限范围内额外返回 `mistakes` 聚合,按场景/归因统计次数、影响人数、平均分和最近发生时间;普通员工返回“无主管权限”。主管接口以后端当前登录手机号映射在职组织快照,仅允许岗位为“主管/项目经理”的账号,并按租户和项目范围返回真实成员、全状态训练记录及非 daily 专项。复盘标记只允许首次 `待复盘 -> 已复盘` 创建后续专项,并发重复提交幂等;可带 `incentivePoint` 写入贡献度 | | 移动端员工训练与主管复盘闭环 | 员工复用 `POST /api/train/practice/start`、`/turn`、`/finish`,查询 `GET /api/aihr/mobile/practice/history`、`/practice/mistakes`、`/profile`;训练完成后提交 `POST /api/aihr/mobile/practice/satisfaction`;主管查询 `GET /api/aihr/mobile/practice/team`、`/practice/alerts`、`/practice/reviews`、`/practice/reviews/{id}`,标记 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed`,指派 `POST /api/aihr/mobile/practice/assignments` | 员工端登录后带 `Authorization` 与 `clientid` 调用;`mode=mobile` 完成后写入 `aihr_practice_session`。满意度接口只接受本人已完成训练的 1-5 分,意见脱敏后落库,未填写不补默认值。错题本按员工本人聚合低分/红线回合,并关联已有 `retry` assignment,不伪造错题结论。主管 `/practice/team` 在同一项目权限范围内额外返回 `mistakes` 聚合,按场景/归因统计次数、影响人数、平均分和最近发生时间;普通员工返回“无主管权限”。主管接口以后端当前登录手机号映射在职组织快照,仅允许岗位为“主管/项目经理”的账号,并按租户和项目范围返回真实成员、全状态训练记录及非 daily 专项。复盘标记只允许首次 `待复盘 -> 已复盘` 创建后续专项,并发重复提交幂等;可带 `incentivePoint` 写入贡献度。每日三题正式只对 `hire_date` 在当前日期前三个月内的在职员工派发;没有入职日期时不使用训练次数推断,开发 Demo 的旧回退由 `aihr.practice.allow-legacy-daily-drill-fallback` 显式开启。 |
| 移动端候选人闭环 | 页面拆为 `/pages/candidate/index/index`、`/interview/index`、`/materials/index`、`/progress/index`、`/study/index`;面试复用 `POST /api/recruit/interview/start`、`/answer`、`/finish` 和 `GET /records`;资料 `POST/GET /api/aihr/mobile/candidate/materials`;预习 `POST /api/knowledge/search`;HR 审核 `GET /api/aihr/hr/candidate/materials`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 候选人端登录后带 `Authorization` 与 `clientid` 调用;首页只按当前手机号对应的真实面试记录和最新资料状态分流,不读取公开 home seed。APP 候选人身份以后端登录手机号为准,前端 `candidateId/candidateName` 只作非 APP 场景兼容参数;`/answer` 与 `/finish` 还会校验当前 APP 手机号与启动会话的候选人 ID 一致,未知或他人会话直接拒绝;面试拉题/评分走真实模型优先;资料写 `sys_oss`/MinIO 和 `aihr_candidate_material`,HR 审核后进度页同步三态;岗前预习查询正式 SOP,不兜前端示例答案 | | 移动端候选人闭环 | 页面拆为 `/pages/candidate/index/index`、`/interview/index`、`/materials/index`、`/progress/index`、`/study/index`;面试复用 `POST /api/recruit/interview/start`、`/answer`、`/finish` 和 `GET /records`;资料 `POST/GET /api/aihr/mobile/candidate/materials`;预习 `POST /api/knowledge/search`;HR 审核 `GET /api/aihr/hr/candidate/materials`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 候选人端登录后带 `Authorization` 与 `clientid` 调用;首页只按当前手机号对应的真实面试记录和最新资料状态分流,不读取公开 home seed。APP 候选人身份以后端登录手机号为准,前端 `candidateId/candidateName` 只作非 APP 场景兼容参数;`/answer` 与 `/finish` 还会校验当前 APP 手机号与启动会话的候选人 ID 一致,未知或他人会话直接拒绝;面试拉题/评分走真实模型优先;资料写 `sys_oss`/MinIO 和 `aihr_candidate_material`,HR 审核后进度页同步三态;岗前预习查询正式 SOP,不兜前端示例答案 |
## 后端落点 ## 后端落点
+2
View File
@@ -263,3 +263,5 @@
- 2026-07-14 BRD G1/4.5 证据包评分边界修复:成长/晋升证据包此前只展示 AI 测评分,人工校准结果仅存在于试点 CSV;现复用 `aihr_practice_calibration` 最新记录,向管理端和员工端同时返回人工校准分、最终采用分和校准样本数,并在导出证据包中保留三项字段。未校准时明确显示“待采集”,不把 AI 分伪装成人工结论;AI 仍仅作训练参考,不直接生成绩效或晋升结论。 - 2026-07-14 BRD G1/4.5 证据包评分边界修复:成长/晋升证据包此前只展示 AI 测评分,人工校准结果仅存在于试点 CSV;现复用 `aihr_practice_calibration` 最新记录,向管理端和员工端同时返回人工校准分、最终采用分和校准样本数,并在导出证据包中保留三项字段。未校准时明确显示“待采集”,不把 AI 分伪装成人工结论;AI 仍仅作训练参考,不直接生成绩效或晋升结论。
- 2026-07-14 BRD 4.5/4.7 案例贡献证据边界复核:现有 `aihr_case_record.owner` 仅保存负责人展示值(系统用户或员工末四位),没有稳定 `ext_party_id`、采纳审核任务或 `incentive_point.source='case_contribution'` 关联;当前案例上传/入库数量不能安全算作员工案例贡献。成长证据包现明确提示“案例上传/入库”与“正式采纳、署名、积分”分离,未把案例数量伪装成晋升证据;待 HR 确认 `review_task` 终审、署名和积分规则后,再设计正式关联与统计。 - 2026-07-14 BRD 4.5/4.7 案例贡献证据边界复核:现有 `aihr_case_record.owner` 仅保存负责人展示值(系统用户或员工末四位),没有稳定 `ext_party_id`、采纳审核任务或 `incentive_point.source='case_contribution'` 关联;当前案例上传/入库数量不能安全算作员工案例贡献。成长证据包现明确提示“案例上传/入库”与“正式采纳、署名、积分”分离,未把案例数量伪装成晋升证据;待 HR 确认 `review_task` 终审、署名和积分规则后,再设计正式关联与统计。
- 2026-07-14 BRD 4.5/B5 贡献度口径修复:能力画像此前把训练次数和待复盘状态混入 `contribution`,会将训练活跃度误显示为有效贡献;现将贡献度收敛为 `aihr_practice_session.incentive_point` 的人工确认分,训练次数继续单独展示。未新增积分来源或自动案例计分,正式案例贡献仍等待 `review_task`/署名/积分规则。 - 2026-07-14 BRD 4.5/B5 贡献度口径修复:能力画像此前把训练次数和待复盘状态混入 `contribution`,会将训练活跃度误显示为有效贡献;现将贡献度收敛为 `aihr_practice_session.incentive_point` 的人工确认分,训练次数继续单独展示。未新增积分来源或自动案例计分,正式案例贡献仍等待 `review_task`/署名/积分规则。
- 2026-07-14 BRD G3 组织快照质量门禁修复:生产默认关闭 `store-display-fields` 时,`AihrOrgSyncService` 原先在 `orgRow()` 内先清空姓名/部门,再以已屏蔽的 `OrgRow` 计算 `suspectText`,外部姓名/部门中的问号乱码可能被漏报,覆盖写入保护因此不完整。现先基于外部原始字段计算 `suspectText`,再独立保存质量标记;展示字段仍按生产脱敏策略清空,组织表结构和写入字段不变。新增 `AihrOrgSyncServiceTest` 回归和 `demo-check` marker;未修改生产数据。正式同步仍需真实上游快照、手机号映射和法务确认。
- 2026-07-14 BRD 4.3.4 每日三题资格边界修复:`aihr_org_snapshot` 没有 `hire_date` 时,旧逻辑用“移动端训练记录少于 3 次”推断新员工,可能把老员工或临时账号误派每日三题。现仅在本地 `application-dev.yml` 显式开启 `allow-legacy-daily-drill-fallback=true`;生产默认关闭,正式环境必须等组织同步提供 `hire_date` 后再按三个月窗口派发。新增边界测试和 `demo-check` marker;不改数据库结构,不伪造正式新员工数据。
+1 -1
View File
@@ -194,7 +194,7 @@ RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_MATCH=true .
- 案例沉淀:进入 `/knowledge/cases`,上传真实语音 → “AI 整理” → “送审” → “入库”,应看到“已完成闭环”和新增案例记录;上传必须先完成 ASR 转写。 - 案例沉淀:进入 `/knowledge/cases`,上传真实语音 → “AI 整理” → “送审” → “入库”,应看到“已完成闭环”和新增案例记录;上传必须先完成 ASR 转写。
- SOP知识库:进入 `/knowledge/sop`,可上传 txt/md/PDF/Word/Excel/PPT 文档入库;点击“检索” → “生成训练题”,应看到“已完成闭环”、命中数据库 SOP 原文片段和训练题;数据库不可用时页面回退 seed。 - SOP知识库:进入 `/knowledge/sop`,可上传 txt/md/PDF/Word/Excel/PPT 文档入库;点击“检索” → “生成训练题”,应看到“已完成闭环”、命中数据库 SOP 原文片段和训练题;数据库不可用时页面回退 seed。
- 资料处理:进入 `/knowledge/processing`,应看到资料总量、解析任务表、处理链路、规则与风险;可用少量文件验证“批量导入/选择目录/服务端导入”。**批量导入走异步队列**:提交即返回,页面出现“本次批量上传”进度面板(排队/加工中/完成/失败 + 单条重试),后台 worker 并发 2 逐条解析入库;暂存目录默认 `./.data/staging`(`aihr.upload.staging` 覆盖)。服务端导入读取 `./.data/import` 下的相对目录,启动后台任务并在页面显示进度,运行中任务可点“取消”;目录导入的重试粒度是同目录重新导入,批量上传的重试粒度是单文件。 - 资料处理:进入 `/knowledge/processing`,应看到资料总量、解析任务表、处理链路、规则与风险;可用少量文件验证“批量导入/选择目录/服务端导入”。**批量导入走异步队列**:提交即返回,页面出现“本次批量上传”进度面板(排队/加工中/完成/失败 + 单条重试),后台 worker 并发 2 逐条解析入库;暂存目录默认 `./.data/staging`(`aihr.upload.staging` 覆盖)。服务端导入读取 `./.data/import` 下的相对目录,启动后台任务并在页面显示进度,运行中任务可点“取消”;目录导入的重试粒度是同目录重新导入,批量上传的重试粒度是单文件。
- uni-app 员工端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`,手机号登录(dev 验证码固定 `123456`)。今日页进入问师傅、练习、案例素材和个人页;练习页应能完成开始练习、提交回应、结束评分、每日三题提交,个人页同步训练历史和成长证据包。 - uni-app 员工端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`,手机号登录(dev 验证码固定 `123456`)。今日页进入问师傅、练习、案例素材和个人页;练习页应能完成开始练习、提交回应、结束评分、每日三题提交,个人页同步训练历史和成长证据包。每日三题正式按组织快照 `hire_date` 判断入职三个月窗口;本地 Demo 若快照尚无该字段,仅由 `application-dev.yml` 的 `aihr.practice.allow-legacy-daily-drill-fallback=true` 启用训练次数回退,生产默认关闭。
- 移动端登录后若组织快照接口返回 `401/403`,只降级为手动岗位确认,不应清除手机号登录态;岗位确认页仍需允许员工选择“生活顾问”后继续进入业务页。 - 移动端登录后若组织快照接口返回 `401/403`,只降级为手动岗位确认,不应清除手机号登录态;岗位确认页仍需允许员工选择“生活顾问”后继续进入业务页。
- uni-app 主管端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/supervisor/index/index`,应看到团队概览、团队画像、团队预警、指派专项、待复盘和复盘详情;从团队画像派专项后,最近专项应立即回读新记录。 - uni-app 主管端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/supervisor/index/index`,应看到团队概览、团队画像、团队预警、指派专项、待复盘和复盘详情;从团队画像派专项后,最近专项应立即回读新记录。
- uni-app 候选人端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/candidate/index/index`,手机号登录(dev 验证码固定 `123456`)→ “开始面试/面试练习”完成答题评分;“补充资料”选择 PDF/Word/图片后上传,应看到资料状态。Codex 内置浏览器不支持本地文件选择时,用真实 HTTP multipart smoke 代替浏览器文件选择。 - uni-app 候选人端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/candidate/index/index`,手机号登录(dev 验证码固定 `123456`)→ “开始面试/面试练习”完成答题评分;“补充资料”选择 PDF/Word/图片后上传,应看到资料状态。Codex 内置浏览器不支持本地文件选择时,用真实 HTTP multipart smoke 代替浏览器文件选择。
+4
View File
@@ -328,6 +328,8 @@ contains frontend/src/views/admin/org-permission.vue "dryRun ? '同步预检' :
contains frontend/src/views/admin/org-permission.vue "组织同步接口不可用,请检查外部同步配置" contains frontend/src/views/admin/org-permission.vue "组织同步接口不可用,请检查外部同步配置"
contains frontend/src/views/admin/org-permission.vue "row.personName || row.extPartyId" contains frontend/src/views/admin/org-permission.vue "row.personName || row.extPartyId"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java "storeDisplayFields" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java "storeDisplayFields"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java "rows.stream().filter(OrgRow::suspectText)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java "boolean suspectText = hasQuestionMark(projectName)"
contains backend/ruoyi-admin/src/main/resources/application-prod.yml "store-display-fields: false" contains backend/ruoyi-admin/src/main/resources/application-prod.yml "store-display-fields: false"
contains mobile-uni/package.json "aihr-mobile-uni" contains mobile-uni/package.json "aihr-mobile-uni"
@@ -514,6 +516,8 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/control
contains mobile-uni/src/pages/supervisor/review/index.vue "标记已复盘" contains mobile-uni/src/pages/supervisor/review/index.vue "标记已复盘"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "source = 'retry'" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "source = 'retry'"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "isWithinNewHireWindow" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "isWithinNewHireWindow"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "allow-legacy-daily-drill-fallback"
contains backend/ruoyi-admin/src/main/resources/application-dev.yml "allow-legacy-daily-drill-fallback: true"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java "instr(coalesce(person_name, '')" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java "instr(coalesce(person_name, '')"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "maskSensitiveText" contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "maskSensitiveText"
contains mobile-uni/src/services/practice.ts "/api/aihr/mobile/practice/assignments" contains mobile-uni/src/services/practice.ts "/api/aihr/mobile/practice/assignments"