chore: demo fixed-code opt-in flag, sop tweaks and doc sync
短信固定验证码增加 aihr.sms.demo-fixed-code-enabled 显式开关(prod 默认仍禁用,演示环境可显式放行);SOP 服务小调整;mobile base 支持 VITE_BASE;作战清单/TechSpec/API_INTEGRATION/docs README 同步。
This commit is contained in:
@@ -62,11 +62,14 @@ public class CaptchaController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 演示/开发兜底:配置后不真发短信,验证码固定为该值,把短信通道从演示关键路径摘掉。
|
* 演示/开发兜底:配置后不真发短信,验证码固定为该值,把短信通道从演示关键路径摘掉。
|
||||||
* prod profile 下无条件忽略(代码级闸门,防止配置失误造成登录绕过)。
|
* prod profile 仅在 aihr.sms.demo-fixed-code-enabled=true 时临时放行。
|
||||||
*/
|
*/
|
||||||
@Value("${aihr.sms.dev-fixed-code:}")
|
@Value("${aihr.sms.dev-fixed-code:}")
|
||||||
private String smsDevFixedCode;
|
private String smsDevFixedCode;
|
||||||
|
|
||||||
|
@Value("${aihr.sms.demo-fixed-code-enabled:false}")
|
||||||
|
private boolean smsDemoFixedCodeEnabled;
|
||||||
|
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,7 +83,9 @@ public class CaptchaController {
|
|||||||
@NotBlank(message = "{user.phonenumber.not.blank}")
|
@NotBlank(message = "{user.phonenumber.not.blank}")
|
||||||
@Pattern(regexp = RegexConstants.MOBILE, message = "{user.mobile.phone.number.not.valid}") String phonenumber) {
|
@Pattern(regexp = RegexConstants.MOBILE, message = "{user.mobile.phone.number.not.valid}") String phonenumber) {
|
||||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + 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 已启用,短信验证码走固定码,未真实发送短信");
|
log.warn("aihr.sms.dev-fixed-code 已启用,短信验证码走固定码,未真实发送短信");
|
||||||
RedisUtils.setCacheObject(key, smsDevFixedCode, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
RedisUtils.setCacheObject(key, smsDevFixedCode, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||||
return R.ok();
|
return R.ok();
|
||||||
|
|||||||
+11
-5
@@ -111,7 +111,7 @@ public class AihrSopSeedService {
|
|||||||
SopSeed seed = seeds.getOrDefault(requestedCategory, seeds.get("投诉处理 SOP"));
|
SopSeed seed = seeds.getOrDefault(requestedCategory, seeds.get("投诉处理 SOP"));
|
||||||
String category = isBlank(requestedCategory) ? seed.category() : requestedCategory;
|
String category = isBlank(requestedCategory) ? seed.category() : requestedCategory;
|
||||||
String queryText = request == null || request.queryText() == null || request.queryText().isBlank() ? seed.question() : request.queryText();
|
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()) {
|
if (dbResponse != null && !dbResponse.snippets().isEmpty()) {
|
||||||
return dbResponse;
|
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
|
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
|
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 = ?
|
where f.tenant_id = ?
|
||||||
and i.name = ?
|
and (? = '' or i.name = ?)
|
||||||
and match(f.content) against (? in natural language mode)
|
and match(f.content) against (? in natural language mode)
|
||||||
order by score desc, f.idx asc
|
order by score desc, f.idx asc
|
||||||
limit ?
|
limit ?
|
||||||
@@ -1625,7 +1625,7 @@ public class AihrSopSeedService {
|
|||||||
rs.getString("content"),
|
rs.getString("content"),
|
||||||
rs.getInt("idx"),
|
rs.getInt("idx"),
|
||||||
rs.getDouble("score")
|
rs.getDouble("score")
|
||||||
), queryText, TENANT_ID, category, queryText, limit);
|
), queryText, TENANT_ID, category, category, queryText, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<KnowledgeHit> likeHits(String category, String queryText, int limit) {
|
private List<KnowledgeHit> 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
|
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
|
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 = ?
|
where f.tenant_id = ?
|
||||||
and i.name = ?
|
and (? = '' or i.name = ?)
|
||||||
and (%s)
|
and (%s)
|
||||||
order by score desc, f.idx asc
|
order by score desc, f.idx asc
|
||||||
limit ?
|
limit ?
|
||||||
@@ -1660,6 +1660,7 @@ public class AihrSopSeedService {
|
|||||||
terms.stream().map(AihrSopSeedService::likePattern).forEach(args::add);
|
terms.stream().map(AihrSopSeedService::likePattern).forEach(args::add);
|
||||||
args.add(TENANT_ID);
|
args.add(TENANT_ID);
|
||||||
args.add(category);
|
args.add(category);
|
||||||
|
args.add(category);
|
||||||
terms.stream().map(AihrSopSeedService::likePattern).forEach(args::add);
|
terms.stream().map(AihrSopSeedService::likePattern).forEach(args::add);
|
||||||
args.add(limit);
|
args.add(limit);
|
||||||
return jdbcTemplate.query(sql, (rs, rowNum) -> new KnowledgeHit(
|
return jdbcTemplate.query(sql, (rs, rowNum) -> new KnowledgeHit(
|
||||||
@@ -1727,6 +1728,7 @@ public class AihrSopSeedService {
|
|||||||
|
|
||||||
private static SearchResponse responseFromHits(String category, String queryText, List<KnowledgeHit> hits) {
|
private static SearchResponse responseFromHits(String category, String queryText, List<KnowledgeHit> hits) {
|
||||||
KnowledgeHit first = hits.get(0);
|
KnowledgeHit first = hits.get(0);
|
||||||
|
String responseCategory = isBlank(category) ? first.category() : category;
|
||||||
List<SnippetResponse> snippets = hits.stream()
|
List<SnippetResponse> snippets = hits.stream()
|
||||||
.map(hit -> new SnippetResponse("第 " + hit.idx() + " 段:" + hit.title(), hit.content()))
|
.map(hit -> new SnippetResponse("第 " + hit.idx() + " 段:" + hit.title(), hit.content()))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -1736,7 +1738,7 @@ public class AihrSopSeedService {
|
|||||||
}
|
}
|
||||||
return new SearchResponse(
|
return new SearchResponse(
|
||||||
queryText,
|
queryText,
|
||||||
category,
|
responseCategory,
|
||||||
"根据已入库 SOP:" + first.content(),
|
"根据已入库 SOP:" + first.content(),
|
||||||
"引用 " + docs.size() + " 份文档 / " + snippets.size() + " 个片段",
|
"引用 " + docs.size() + " 份文档 / " + snippets.size() + " 个片段",
|
||||||
List.copyOf(docs.values()),
|
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) {
|
private static int limitValue(Integer limit) {
|
||||||
if (limit == null) {
|
if (limit == null) {
|
||||||
return 3;
|
return 3;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# AI 人力资源系统 · 一期 MVP 版作战清单
|
# AI 人力资源系统 · 一期 MVP 版作战清单
|
||||||
|
|
||||||
> 版本:v1.8 | 日期:2026-07-03
|
> 版本:v1.10 | 日期:2026-07-04
|
||||||
> 用途:一份纸面作战图,支撑「2026-07-05 周日出可跑 MVP 演示、2026-07-06 周一给大老板演示」,同时说清「做了什么、没做什么、为什么」。
|
> 用途:一份纸面作战图,支撑「2026-07-05 周日出可跑 MVP 演示、2026-07-06 周一给大老板演示」,同时说清「做了什么、没做什么、为什么」。
|
||||||
> 收敛来源:原《物业管家与生活顾问 AI 陪练系统建设方案》+ 一期实施方案(含 ch.14/15 选型评估)+ 2026-07-01 需求讨论纪要 + 数据资源模型主题域草案。
|
> 收敛来源:原《物业管家与生活顾问 AI 陪练系统建设方案》+ 一期实施方案(含 ch.14/15 选型评估)+ 2026-07-01 需求讨论纪要 + 数据资源模型主题域草案。
|
||||||
> v1.1 变更:D1–D8 决策已定(见第 8 节),相关章节同步锁定;D9(验收标准)待定。
|
> v1.1 变更:D1–D8 决策已定(见第 8 节),相关章节同步锁定;D9(验收标准)待定。
|
||||||
@@ -11,6 +11,8 @@
|
|||||||
> v1.6 变更:Qdrant 本地编排和后端最小闭环已落地;上传后尽力 upsert 向量,检索时与 MySQL Fulltext 做 RRF 融合。
|
> v1.6 变更:Qdrant 本地编排和后端最小闭环已落地;上传后尽力 upsert 向量,检索时与 MySQL Fulltext 做 RRF 融合。
|
||||||
> v1.7 变更:资料处理服务端目录导入已从同步导入升级为后台任务,页面可轮询进度和按同目录重试。
|
> v1.7 变更:资料处理服务端目录导入已从同步导入升级为后台任务,页面可轮询进度和按同目录重试。
|
||||||
> v1.8 变更:补齐 D12「用户端最小闭环」和第 5.1 节;MVP 必须能从候选人/员工视角发起至少 1 条路径,用户端拆为独立 `mobile/` Web/H5 工程,但不做原生 APP。
|
> 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。
|
- 用户端使用独立 `mobile/` Vue/Vite Web/H5 工程,与管理端 `frontend/` 分离;MVP 不做原生 APP。
|
||||||
- MVP 用户端走手机号短信登录;新手机号验证码通过后自动注册为 `app_user`。演示脚本必须从「用户端发起 → 管理端看结果」走一遍。
|
- MVP 用户端走手机号短信登录;新手机号验证码通过后自动注册为 `app_user`。演示脚本必须从「用户端发起 → 管理端看结果」走一遍。
|
||||||
- 当前员工端已用“投诉接待三角色对练”跑通移动端训练闭环:两轮提交后自动评分,记录落 `aihr_practice_session`,并同步更新主管端完训人数、待复盘列表、复盘详情、标记已复盘状态和员工能力画像。
|
- 当前员工端已用“投诉接待三角色对练”跑通移动端训练闭环:两轮提交后自动评分,记录落 `aihr_practice_session`,并同步更新主管端完训人数、待复盘列表、复盘详情、标记已复盘状态和员工能力画像。
|
||||||
|
- 当前候选人端已用“开始面试/面试练习”复用 AI 面试 API;“补充资料”已接认证上传接口,文件写 `sys_oss`/MinIO,关系写 `aihr_candidate_material`,HR 可在管理端 `/recruit/interview` 审核为 `待审核`/`已通过`/`已驳回`,候选人端刷新可见状态。
|
||||||
- 用户端只做「能完成任务」,不做复杂个人中心、社交、积分商城。
|
- 用户端只做「能完成任务」,不做复杂个人中心、社交、积分商城。
|
||||||
|
|
||||||
### 🟡 一期内做(MVP 演示后)
|
### 🟡 一期内做(MVP 演示后)
|
||||||
|
|||||||
+11
-1
@@ -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 代码级强制失效) |
|
| 移动端手机号登录 | `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 保演示 |
|
| 移动端三端首页 `/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/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`
|
- 包名建议:`org.dromara.aihr`
|
||||||
- Controller 返回统一用 `org.dromara.common.core.domain.R`
|
- 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 <access_token>`;浏览器内通过已登录前端和 `/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。
|
直接打后端 `/api/**` 通常需要登录后的 `Authorization: Bearer <access_token>`;浏览器内通过已登录前端和 `/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助手
|
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 文档上传第三片已经落最小后端边界:
|
SOP 文档上传第三片已经落最小后端边界:
|
||||||
|
|
||||||
| 能力 | 后端接口 | 处理 |
|
| 能力 | 后端接口 | 处理 |
|
||||||
@@ -94,6 +103,7 @@ Qdrant 本地默认值可不配;需要覆盖时用 JVM property 或环境变
|
|||||||
- 模型配置页面:`frontend/src/views/system/model/index.vue`
|
- 模型配置页面:`frontend/src/views/system/model/index.vue`
|
||||||
- 移动端独立工程:`mobile/src/App.vue`,路由 `/h5/user`、`/h5/candidate`、`/h5/supervisor`;当前三端首页已按高保真原型实现,未登录先走手机号短信登录,登录后 API 优先请求 `/api/aihr/mobile/home/{role}`,不复用后台页面路由。
|
- 移动端独立工程:`mobile/src/App.vue`,路由 `/h5/user`、`/h5/candidate`、`/h5/supervisor`;当前三端首页已按高保真原型实现,未登录先走手机号短信登录,登录后 API 优先请求 `/api/aihr/mobile/home/{role}`,不复用后台页面路由。
|
||||||
- 员工端训练:任务卡“开始训练”复用管理端三角色对练 API,完成两轮后展示评分和导师改写;记录写入 `aihr_practice_session`,员工端展示训练历史和能力画像,主管端展示待复盘列表,点进单条可看评分、话术、导师改写,并可标记“已复盘”。
|
- 员工端训练:任务卡“开始训练”复用管理端三角色对练 API,完成两轮后展示评分和导师改写;记录写入 `aihr_practice_session`,员工端展示训练历史和能力画像,主管端展示待复盘列表,点进单条可看评分、话术、导师改写,并可标记“已复盘”。
|
||||||
|
- 候选人端:`/h5/candidate` 的“开始面试/面试练习”复用 AI 面试 API;“补充资料”走候选资料上传接口,成功后展示 OSS ID、资料类型和审核状态。管理端 `/recruit/interview` 底部展示“候选资料审核”,支持通过/驳回。
|
||||||
- 演示 fallback 只作为未配置模型或外部接口异常时的降级,不作为真实测试通过证据。
|
- 演示 fallback 只作为未配置模型或外部接口异常时的降级,不作为真实测试通过证据。
|
||||||
|
|
||||||
## 验收
|
## 验收
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| [DEV_SETUP.md](DEV_SETUP.md) | 本地端口、账号、启动步骤、基础链路验证 |
|
| [DEV_SETUP.md](DEV_SETUP.md) | 本地端口、账号、启动步骤、基础链路验证 |
|
||||||
| [DEMO_ACCEPTANCE.md](DEMO_ACCEPTANCE.md) | 一期 MVP 演示脚本、录屏兜底、MVP 演示流验收清单 |
|
| [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 的执行边界 |
|
| [RUOYI_AI_INCREMENTAL_MIGRATION.md](RUOYI_AI_INCREMENTAL_MIGRATION.md) | 从 `ageerle/ruoyi-ai` 分片迁移知识库、模型能力、文档解析、Qdrant/RAG 和 chat 的执行边界 |
|
||||||
| [AI人力资源系统一期MVP版作战清单.md](AI人力资源系统一期MVP版作战清单.md) | MVP 作战图、切割线、排期、验收承诺 |
|
| [AI人力资源系统一期MVP版作战清单.md](AI人力资源系统一期MVP版作战清单.md) | MVP 作战图、切割线、排期、验收承诺 |
|
||||||
| [物业AI人力资源系统业务需求文档BRD.md](物业AI人力资源系统业务需求文档BRD.md) | 业务需求、范围边界、角色、风险与验收 |
|
| [物业AI人力资源系统业务需求文档BRD.md](物业AI人力资源系统业务需求文档BRD.md) | 业务需求、范围边界、角色、风险与验收 |
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ mobile/
|
|||||||
| 表 | 关键字段 | 优先级 |
|
| 表 | 关键字段 | 优先级 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `candidate`(候选人) | id, name, phone, apply_position_code, project_ext_org_id, source, status, hired_ext_party_id(入职后挂钩) | P0 |
|
| `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 |
|
| `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_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 |
|
| `interview_question`(面试题) | id, session_id, seq, question_text, gen_source(ai/manual), reference_point | P0 |
|
||||||
@@ -173,6 +174,8 @@ mobile/
|
|||||||
| `/recruit/candidates` | 候选人列表 | P1 |
|
| `/recruit/candidates` | 候选人列表 | P1 |
|
||||||
| `/train/practice` | **三角色对练(英雄路径核心)** | P0跑通 |
|
| `/train/practice` | **三角色对练(英雄路径核心)** | P0跑通 |
|
||||||
| `/h5/user` | 员工端首页,手机号登录后可“开始训练”并同步主管待复盘计数 | P0跑通 |
|
| `/h5/user` | 员工端首页,手机号登录后可“开始训练”并同步主管待复盘计数 | P0跑通 |
|
||||||
|
| `/h5/candidate` | 候选人端首页,手机号登录后可面试练习、查岗位 SOP/案例、上传补充资料 | P0跑通 |
|
||||||
|
| `/h5/supervisor` | 主管端首页,查看团队、待复盘、案例/SOP 工具 | P0跑通 |
|
||||||
| `/train/daily` | 每日一练 | P1 |
|
| `/train/daily` | 每日一练 | P1 |
|
||||||
| `/train/camp` | 专项训练营 | P1 |
|
| `/train/camp` | 专项训练营 | P1 |
|
||||||
| `/train/mistakes` | 错题本 | P1 |
|
| `/train/mistakes` | 错题本 | P1 |
|
||||||
@@ -207,8 +210,24 @@ POST /api/recruit/interview/answer
|
|||||||
POST /api/recruit/interview/finish
|
POST /api/recruit/interview/finish
|
||||||
req: { sessionId }
|
req: { sessionId }
|
||||||
resp: { totalScore, aiSummary, records:[...] }
|
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)
|
### 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 |
|
| 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、企微/钉钉集成表 |
|
| P2 | 知识图谱相关、SSO、企微/钉钉集成表 |
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import vue from '@vitejs/plugin-vue';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: '/',
|
base: process.env.VITE_BASE || '/',
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src')
|
'@': path.resolve(__dirname, './src')
|
||||||
|
|||||||
Reference in New Issue
Block a user