From 775873e31bff1d188af92b4a6d5653cada0993f7 Mon Sep 17 00:00:00 2001 From: let5sne Date: Sat, 4 Jul 2026 16:05:50 +0800 Subject: [PATCH] chore: demo fixed-code opt-in flag, sop tweaks and doc sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 短信固定验证码增加 aihr.sms.demo-fixed-code-enabled 显式开关(prod 默认仍禁用,演示环境可显式放行);SOP 服务小调整;mobile base 支持 VITE_BASE;作战清单/TechSpec/API_INTEGRATION/docs README 同步。 --- .../web/controller/CaptchaController.java | 9 ++++++-- .../aihr/service/AihrSopSeedService.java | 16 +++++++++----- docs/AI人力资源系统一期MVP版作战清单.md | 5 ++++- docs/API_INTEGRATION.md | 12 ++++++++++- docs/README.md | 2 +- docs/物业AI人力资源系统开发规格TechSpec.md | 21 ++++++++++++++++++- mobile/vite.config.ts | 2 +- 7 files changed, 55 insertions(+), 12 deletions(-) diff --git a/backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java b/backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java index 80244362..5990cd86 100644 --- a/backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java +++ b/backend/ruoyi-admin/src/main/java/org/dromara/web/controller/CaptchaController.java @@ -62,11 +62,14 @@ public class CaptchaController { /** * 演示/开发兜底:配置后不真发短信,验证码固定为该值,把短信通道从演示关键路径摘掉。 - * prod profile 下无条件忽略(代码级闸门,防止配置失误造成登录绕过)。 + * prod profile 仅在 aihr.sms.demo-fixed-code-enabled=true 时临时放行。 */ @Value("${aihr.sms.dev-fixed-code:}") private String smsDevFixedCode; + @Value("${aihr.sms.demo-fixed-code-enabled:false}") + private boolean smsDemoFixedCodeEnabled; + private final Environment environment; /** @@ -80,7 +83,9 @@ public class CaptchaController { @NotBlank(message = "{user.phonenumber.not.blank}") @Pattern(regexp = RegexConstants.MOBILE, message = "{user.mobile.phone.number.not.valid}") String phonenumber) { String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber; - if (StringUtils.isNotBlank(smsDevFixedCode) && !environment.acceptsProfiles(Profiles.of("prod"))) { + boolean fixedCodeEnabled = StringUtils.isNotBlank(smsDevFixedCode) + && (!environment.acceptsProfiles(Profiles.of("prod")) || smsDemoFixedCodeEnabled); + if (fixedCodeEnabled) { log.warn("aihr.sms.dev-fixed-code 已启用,短信验证码走固定码,未真实发送短信"); RedisUtils.setCacheObject(key, smsDevFixedCode, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); return R.ok(); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java index ed0b42c4..60d5424f 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java @@ -111,7 +111,7 @@ public class AihrSopSeedService { SopSeed seed = seeds.getOrDefault(requestedCategory, seeds.get("投诉处理 SOP")); String category = isBlank(requestedCategory) ? seed.category() : requestedCategory; String queryText = request == null || request.queryText() == null || request.queryText().isBlank() ? seed.question() : request.queryText(); - SearchResponse dbResponse = dbSearch(category, queryText, request == null ? null : request.limit()); + SearchResponse dbResponse = dbSearch(dbCategory(category), queryText, request == null ? null : request.limit()); if (dbResponse != null && !dbResponse.snippets().isEmpty()) { return dbResponse; } @@ -1613,7 +1613,7 @@ public class AihrSopSeedService { join aihr_knowledge_info i on i.id = f.knowledge_id and i.tenant_id = f.tenant_id left join aihr_knowledge_attach a on a.knowledge_id = f.knowledge_id and a.doc_id = f.doc_id and a.tenant_id = f.tenant_id where f.tenant_id = ? - and i.name = ? + and (? = '' or i.name = ?) and match(f.content) against (? in natural language mode) order by score desc, f.idx asc limit ? @@ -1625,7 +1625,7 @@ public class AihrSopSeedService { rs.getString("content"), rs.getInt("idx"), rs.getDouble("score") - ), queryText, TENANT_ID, category, queryText, limit); + ), queryText, TENANT_ID, category, category, queryText, limit); } private List likeHits(String category, String queryText, int limit) { @@ -1651,7 +1651,7 @@ public class AihrSopSeedService { join aihr_knowledge_info i on i.id = f.knowledge_id and i.tenant_id = f.tenant_id left join aihr_knowledge_attach a on a.knowledge_id = f.knowledge_id and a.doc_id = f.doc_id and a.tenant_id = f.tenant_id where f.tenant_id = ? - and i.name = ? + and (? = '' or i.name = ?) and (%s) order by score desc, f.idx asc limit ? @@ -1660,6 +1660,7 @@ public class AihrSopSeedService { terms.stream().map(AihrSopSeedService::likePattern).forEach(args::add); args.add(TENANT_ID); args.add(category); + args.add(category); terms.stream().map(AihrSopSeedService::likePattern).forEach(args::add); args.add(limit); return jdbcTemplate.query(sql, (rs, rowNum) -> new KnowledgeHit( @@ -1727,6 +1728,7 @@ public class AihrSopSeedService { private static SearchResponse responseFromHits(String category, String queryText, List hits) { KnowledgeHit first = hits.get(0); + String responseCategory = isBlank(category) ? first.category() : category; List snippets = hits.stream() .map(hit -> new SnippetResponse("第 " + hit.idx() + " 段:" + hit.title(), hit.content())) .toList(); @@ -1736,7 +1738,7 @@ public class AihrSopSeedService { } return new SearchResponse( queryText, - category, + responseCategory, "根据已入库 SOP:" + first.content(), "引用 " + docs.size() + " 份文档 / " + snippets.size() + " 个片段", List.copyOf(docs.values()), @@ -1746,6 +1748,10 @@ public class AihrSopSeedService { ); } + private static String dbCategory(String category) { + return "sop".equalsIgnoreCase(Optional.ofNullable(category).orElse("").trim()) ? "" : category; + } + private static int limitValue(Integer limit) { if (limit == null) { return 3; diff --git a/docs/AI人力资源系统一期MVP版作战清单.md b/docs/AI人力资源系统一期MVP版作战清单.md index 946e9b6e..5d61d903 100644 --- a/docs/AI人力资源系统一期MVP版作战清单.md +++ b/docs/AI人力资源系统一期MVP版作战清单.md @@ -1,6 +1,6 @@ # AI 人力资源系统 · 一期 MVP 版作战清单 -> 版本:v1.8 | 日期:2026-07-03 +> 版本:v1.10 | 日期:2026-07-04 > 用途:一份纸面作战图,支撑「2026-07-05 周日出可跑 MVP 演示、2026-07-06 周一给大老板演示」,同时说清「做了什么、没做什么、为什么」。 > 收敛来源:原《物业管家与生活顾问 AI 陪练系统建设方案》+ 一期实施方案(含 ch.14/15 选型评估)+ 2026-07-01 需求讨论纪要 + 数据资源模型主题域草案。 > v1.1 变更:D1–D8 决策已定(见第 8 节),相关章节同步锁定;D9(验收标准)待定。 @@ -11,6 +11,8 @@ > v1.6 变更:Qdrant 本地编排和后端最小闭环已落地;上传后尽力 upsert 向量,检索时与 MySQL Fulltext 做 RRF 融合。 > v1.7 变更:资料处理服务端目录导入已从同步导入升级为后台任务,页面可轮询进度和按同目录重试。 > v1.8 变更:补齐 D12「用户端最小闭环」和第 5.1 节;MVP 必须能从候选人/员工视角发起至少 1 条路径,用户端拆为独立 `mobile/` Web/H5 工程,但不做原生 APP。 +> v1.9 变更:候选人端面试练习已复用 AI 面试 API;补充资料已从本地选择升级为 OSS + `aihr_candidate_material` 入库,默认状态为 `待审核`。 +> v1.10 变更:候选资料 HR 审核流已接入管理端 AI 面试页;支持按状态查看并标记 `已通过`/`已驳回`,候选人端刷新可见新状态。 --- @@ -152,6 +154,7 @@ - 用户端使用独立 `mobile/` Vue/Vite Web/H5 工程,与管理端 `frontend/` 分离;MVP 不做原生 APP。 - MVP 用户端走手机号短信登录;新手机号验证码通过后自动注册为 `app_user`。演示脚本必须从「用户端发起 → 管理端看结果」走一遍。 - 当前员工端已用“投诉接待三角色对练”跑通移动端训练闭环:两轮提交后自动评分,记录落 `aihr_practice_session`,并同步更新主管端完训人数、待复盘列表、复盘详情、标记已复盘状态和员工能力画像。 +- 当前候选人端已用“开始面试/面试练习”复用 AI 面试 API;“补充资料”已接认证上传接口,文件写 `sys_oss`/MinIO,关系写 `aihr_candidate_material`,HR 可在管理端 `/recruit/interview` 审核为 `待审核`/`已通过`/`已驳回`,候选人端刷新可见状态。 - 用户端只做「能完成任务」,不做复杂个人中心、社交、积分商城。 ### 🟡 一期内做(MVP 演示后) diff --git a/docs/API_INTEGRATION.md b/docs/API_INTEGRATION.md index 534a1e81..9431dbfd 100644 --- a/docs/API_INTEGRATION.md +++ b/docs/API_INTEGRATION.md @@ -15,6 +15,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 代码级强制失效) | | 移动端三端首页 `/h5/user`、`/h5/candidate`、`/h5/supervisor` | `GET /api/aihr/mobile/home/{role}` | 已接入员工、候选人、主管首页公开只读 API;移动端本地 fallback 保演示 | | 移动端员工训练闭环 | 复用 `POST /api/train/practice/start`、`/turn`、`/finish`;查询 `GET /api/aihr/mobile/practice/history`、`/practice/reviews`、`/practice/reviews/{id}`、`/profile`;标记 `POST /api/aihr/mobile/practice/reviews/{id}/reviewed` | 员工端登录后带 `Authorization` 与 `clientid` 调用;`mode=mobile` 完成后写入 `aihr_practice_session`,主管端首页完训率、待复盘列表、复盘详情、员工训练历史和能力画像同步变化 | +| 移动端候选人闭环 | 面试复用 `POST /api/recruit/interview/start`、`/answer`、`/finish`;资料 `POST /api/aihr/mobile/candidate/materials`、`GET /api/aihr/mobile/candidate/materials`;HR 审核 `GET /api/aihr/hr/candidate/materials`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 候选人端登录后带 `Authorization` 与 `clientid` 调用;面试拉题/评分走真实模型优先;资料上传写 `sys_oss`/MinIO 和 `aihr_candidate_material`,HR 在 AI 面试页标记 `已通过/已驳回`,候选人端列表同步状态 | ## 后端落点 @@ -24,7 +25,7 @@ - 包名建议:`org.dromara.aihr` - Controller 返回统一用 `org.dromara.common.core.domain.R` -当前管理端 AI 面试和案例沉淀已改为真实模型/ASR 优先;移动端首页和部分演示态数据仍保留 fallback。知识库、模型能力、文档解析、RAG、chat 按 [ruoyi-ai 能力分片迁移计划](RUOYI_AI_INCREMENTAL_MIGRATION.md) 逐片引入;知识库 DDL 与住宅类 SOP seed 在 `backend/script/sql/aihr_knowledge_mysql8.sql`,模型 DDL 在 `backend/script/sql/aihr_model_mysql8.sql`,训练记录 DDL 在 `backend/script/sql/aihr_practice_mysql8.sql`,组织人员静态快照(2 个住宅项目 22 人,支撑按项目看人数)在 `backend/script/sql/aihr_org_snapshot_mysql8.sql`。 +当前管理端 AI 面试和案例沉淀已改为真实模型/ASR 优先;移动端首页和部分演示态数据仍保留 fallback。知识库、模型能力、文档解析、RAG、chat 按 [ruoyi-ai 能力分片迁移计划](RUOYI_AI_INCREMENTAL_MIGRATION.md) 逐片引入;知识库 DDL 与住宅类 SOP seed 在 `backend/script/sql/aihr_knowledge_mysql8.sql`,模型 DDL 在 `backend/script/sql/aihr_model_mysql8.sql`,训练记录 DDL 在 `backend/script/sql/aihr_practice_mysql8.sql`,AI 面试结果 DDL 在 `backend/script/sql/aihr_interview_result_mysql8.sql`,候选人资料 DDL 在 `backend/script/sql/aihr_candidate_material_mysql8.sql`,组织人员静态快照(2 个住宅项目 22 人,支撑按项目看人数)在 `backend/script/sql/aihr_org_snapshot_mysql8.sql`。 直接打后端 `/api/**` 通常需要登录后的 `Authorization: Bearer `;浏览器内通过已登录前端和 `/dev-api` 代理访问。移动端登录接口为 `POST /auth/mobile/sms-login`,请求 `{ phonenumber, smsCode, tenantId }`,内部固定使用 app 客户端 `428a8310cd442757ae699df5d894f051` 和 `sms` grant;验证码通过后若手机号不存在,会创建 `app_user`,用户名为手机号,备注为“移动端短信自动注册”。移动端 MVP 首页接口 `GET /api/aihr/mobile/home/{role}` 目前仍是 `@SaIgnore` 的公开只读 seed 接口,避免 H5 首屏被后台管理登录态阻断;后续接小程序登录后再收紧为移动端 token。 @@ -37,6 +38,14 @@ ALIYUN_SMS_ACCESS_KEY_SECRET=xxx ALIYUN_SMS_SIGN_NAME=物业AI助手 ``` +候选人资料上传最小后端边界: + +| 能力 | 后端接口 | 处理 | +|---|---|---| +| 候选资料上传 | `POST /api/aihr/mobile/candidate/materials` | `multipart/form-data` 字段 `file`,参数 `candidateId/candidateName/materialType`;需移动端登录返回的 `Authorization` 与 `clientid`;文件用 `ISysOssService` 写 `sys_oss`/MinIO,关系写 `aihr_candidate_material`,状态默认 `待审核`,单文件 20MB | +| 候选资料列表 | `GET /api/aihr/mobile/candidate/materials?candidateId=` | 返回前 20 条 `{id, ossId, fileName, materialType, fileSize, status, time}`,供 `/h5/candidate` 的“已提交资料”展示 | +| HR 资料审核 | `GET /api/aihr/hr/candidate/materials?status=`、`POST /api/aihr/hr/candidate/materials/{id}/review` | 管理端 `/recruit/interview` 展示前 50 条,可按状态筛选;审核请求体 `{status}`,只接受 `待审核/已通过/已驳回` | + SOP 文档上传第三片已经落最小后端边界: | 能力 | 后端接口 | 处理 | @@ -94,6 +103,7 @@ Qdrant 本地默认值可不配;需要覆盖时用 JVM property 或环境变 - 模型配置页面:`frontend/src/views/system/model/index.vue` - 移动端独立工程:`mobile/src/App.vue`,路由 `/h5/user`、`/h5/candidate`、`/h5/supervisor`;当前三端首页已按高保真原型实现,未登录先走手机号短信登录,登录后 API 优先请求 `/api/aihr/mobile/home/{role}`,不复用后台页面路由。 - 员工端训练:任务卡“开始训练”复用管理端三角色对练 API,完成两轮后展示评分和导师改写;记录写入 `aihr_practice_session`,员工端展示训练历史和能力画像,主管端展示待复盘列表,点进单条可看评分、话术、导师改写,并可标记“已复盘”。 +- 候选人端:`/h5/candidate` 的“开始面试/面试练习”复用 AI 面试 API;“补充资料”走候选资料上传接口,成功后展示 OSS ID、资料类型和审核状态。管理端 `/recruit/interview` 底部展示“候选资料审核”,支持通过/驳回。 - 演示 fallback 只作为未配置模型或外部接口异常时的降级,不作为真实测试通过证据。 ## 验收 diff --git a/docs/README.md b/docs/README.md index 6dcbacfc..a989effb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ |---|---| | [DEV_SETUP.md](DEV_SETUP.md) | 本地端口、账号、启动步骤、基础链路验证 | | [DEMO_ACCEPTANCE.md](DEMO_ACCEPTANCE.md) | 一期 MVP 演示脚本、录屏兜底、MVP 演示流验收清单 | -| [API_INTEGRATION.md](API_INTEGRATION.md) | 后端 API 对接顺序、业务模块落点、移动端训练记录/复盘详情、SOP 上传解析接口 | +| [API_INTEGRATION.md](API_INTEGRATION.md) | 后端 API 对接顺序、业务模块落点、移动端训练记录/复盘详情、候选资料上传/审核、SOP 上传解析接口 | | [RUOYI_AI_INCREMENTAL_MIGRATION.md](RUOYI_AI_INCREMENTAL_MIGRATION.md) | 从 `ageerle/ruoyi-ai` 分片迁移知识库、模型能力、文档解析、Qdrant/RAG 和 chat 的执行边界 | | [AI人力资源系统一期MVP版作战清单.md](AI人力资源系统一期MVP版作战清单.md) | MVP 作战图、切割线、排期、验收承诺 | | [物业AI人力资源系统业务需求文档BRD.md](物业AI人力资源系统业务需求文档BRD.md) | 业务需求、范围边界、角色、风险与验收 | diff --git a/docs/物业AI人力资源系统开发规格TechSpec.md b/docs/物业AI人力资源系统开发规格TechSpec.md index 0c864ff3..3c0734a6 100644 --- a/docs/物业AI人力资源系统开发规格TechSpec.md +++ b/docs/物业AI人力资源系统开发规格TechSpec.md @@ -97,6 +97,7 @@ mobile/ | 表 | 关键字段 | 优先级 | |---|---|---| | `candidate`(候选人) | id, name, phone, apply_position_code, project_ext_org_id, source, status, hired_ext_party_id(入职后挂钩) | P0 | +| `aihr_candidate_material`(候选人补充资料) | id, candidate_id, candidate_name, material_type, file_name, file_size, content_type, oss_id, status, create_time | P0 | | `job_requisition`(招聘需求) | id, position_code, project_ext_org_id, headcount, jd_text, status | P1 | | `interview_session`(面试记录) | id, candidate_id, position_code, mode(voice/text), total_score, ai_summary, status, started_at | P0 | | `interview_question`(面试题) | id, session_id, seq, question_text, gen_source(ai/manual), reference_point | P0 | @@ -173,6 +174,8 @@ mobile/ | `/recruit/candidates` | 候选人列表 | P1 | | `/train/practice` | **三角色对练(英雄路径核心)** | P0跑通 | | `/h5/user` | 员工端首页,手机号登录后可“开始训练”并同步主管待复盘计数 | P0跑通 | +| `/h5/candidate` | 候选人端首页,手机号登录后可面试练习、查岗位 SOP/案例、上传补充资料 | P0跑通 | +| `/h5/supervisor` | 主管端首页,查看团队、待复盘、案例/SOP 工具 | P0跑通 | | `/train/daily` | 每日一练 | P1 | | `/train/camp` | 专项训练营 | P1 | | `/train/mistakes` | 错题本 | P1 | @@ -207,8 +210,24 @@ POST /api/recruit/interview/answer POST /api/recruit/interview/finish req: { sessionId } resp: { totalScore, aiSummary, records:[...] } + +POST /api/aihr/mobile/candidate/materials + req: multipart file + candidateId + candidateName + materialType + resp: { id, ossId, fileName, materialType, fileSize, status, time } + +GET /api/aihr/mobile/candidate/materials?candidateId= + resp: [{ id, ossId, fileName, materialType, fileSize, status, time }] + +GET /api/aihr/hr/candidate/materials?status= + resp: [{ id, candidateId, candidateName, ossId, fileName, materialType, fileSize, status, time }] + +POST /api/aihr/hr/candidate/materials/{id}/review + req: { status } // 待审核 / 已通过 / 已驳回 + resp: { id, candidateId, candidateName, ossId, fileName, materialType, fileSize, status, time } ``` +候选资料上传使用移动端登录返回的 `Authorization` 与 `clientid`;文件先经 `sys_oss`/MinIO 持久化,再写 `aihr_candidate_material` 关系行,默认状态 `待审核`。 + ### 5.2 英雄路径核心:三角色对练(P0) ``` @@ -395,7 +414,7 @@ INIT → WAITING_INPUT → PROCESSING → WAITING_INPUT(loop) → FINISHED | 优先级 | 表 | |---|---| -| P0 | sync_person, sync_org_unit, sync_batch, party_role, role_perm_mapping, candidate, interview_session, interview_question, interview_answer, practice_scenario, practice_session, practice_turn, practice_score, rubric, rubric_dimension, knowledge_doc, knowledge_chunk, case, asr_config | +| P0 | sync_person, sync_org_unit, sync_batch, party_role, role_perm_mapping, candidate, aihr_candidate_material, interview_session, interview_question, interview_answer, practice_scenario, practice_session, practice_turn, practice_score, rubric, rubric_dimension, knowledge_doc, knowledge_chunk, case, asr_config | | P1 | job_requisition, sop_category, sop, sop_applicability, onboard_task, course, course_enrollment, qualification_gate, daily_drill, training_camp, mistake_book, mentor_assignment, competency_assessment, competency_dim_config, certification, cert_rule, incentive_point, badge, case_tag, case_video, review_task, content_version, llm_call_log | | P2 | 知识图谱相关、SSO、企微/钉钉集成表 | diff --git a/mobile/vite.config.ts b/mobile/vite.config.ts index b542035d..eecd8d89 100644 --- a/mobile/vite.config.ts +++ b/mobile/vite.config.ts @@ -3,7 +3,7 @@ import vue from '@vitejs/plugin-vue'; import path from 'path'; export default defineConfig({ - base: '/', + base: process.env.VITE_BASE || '/', resolve: { alias: { '@': path.resolve(__dirname, './src')