Files
prop-ai-hr/docs/DEV_SETUP.md
T

37 KiB
Raw Blame History

帮道 MVP 本地启动

当前仓库

  • 后端:backend,来自 dromara/RuoYi-Vue-Plus 的 5.X 分支
  • 管理端前端:frontend,来自 CrazyLionCat/plus-ui 的 5.X 分支
  • 用户侧前端:mobile-uni,uni-app Vue3 H5 + App-Plus 工程;旧 mobile 仅保留为 MVP 演示兜底

本地入口与依赖端口

  • 管理端前端:https://wygj-admin.localhost/
  • uni-app 用户侧:https://wygj-mobile-uni.localhost/h5/
  • 后端:https://wygj-api.localhost/
  • 固定端口仅用于显式 fallback:PORTLESS=0 ./scripts/dev.sh 时后端回退 http://127.0.0.1:8080/,旧移动端回退 http://127.0.0.1:5174/h5/user
  • MySQL:127.0.0.1:13306,数据库 ry-vue,账号 root/root
  • Redis:127.0.0.1:16379,密码 ruoyi123
  • MinIO API:http://127.0.0.1:9000,Console:http://127.0.0.1:9001,账号 ruoyi / ruoyi123,默认 bucket ruoyi
  • Qdrant REST:http://127.0.0.1:6333,默认 collection aihr_knowledge
  • 服务端资料导入根目录:./.data/import

portless 本地入口

本机开发测试优先用 portless 访问稳定域名,避免继续记固定端口:

  • uni-app 用户侧:https://wygj-mobile-uni.localhost/h5/
  • 管理端:https://wygj-admin.localhost/
  • 后端 API:https://wygj-api.localhost

Linux/macOS 或已完整配置的 Linux shell 可从根目录一次性启动本地依赖、后端、管理端和 uni-app 用户侧:

./scripts/dev.sh

./scripts/dev.sh 默认通过 portless 启动后端、管理端和 mobile-uni,并由 Docker 启动 MySQL/Redis/MinIO/Qdrant。管理端 Vite 代理读取 frontend/.env.development 的 VITE_APP_PROXY_TARGET=https://wygj-api.localhost,mobile-uni Vite 代理默认指向 https://wygj-api.localhost,都不依赖固定 8080。如需单独启动后端,可执行:

portless run --name wygj-api ./scripts/dev-backend.sh

Windows 10/11 + Docker Desktop 不要从 PowerShell 直接执行 ./scripts/dev.sh:Windows 的 bash 可能解析到 WSL,而 WSL 的 Node、Docker 集成和检出文件换行符未必满足脚本要求。使用本文件的 Windows 10/11 + Docker Desktop 流程。

本地启动默认关闭管理端图形验证码;需要专门验证验证码链路时设置 AIHR_DEV_CAPTCHA_ENABLED=true。生产启动不使用该开发开关,验证码保持开启。

重复启动或切换分支时优先使用 ./scripts/dev.sh restart,停止使用 ./scripts/dev.sh stop。脚本会递归终止 portless 包装进程及其 Java/Vite 子进程,避免旧后端脱离后继续占用 MySQL 连接池;进程树回归可用 bash scripts/tests/process-tree.test.sh 验证。

旧 mobile/ 只在显式 PORTLESS=0 ./scripts/dev.sh 时作为固定端口兜底启动;后端开发测试优先看 wygj-api.localhost,管理端开发测试优先看 wygj-admin.localhost,新用户侧开发测试优先看 wygj-mobile-uni.localhost。

原生 Android/iOS 资源使用 npm --prefix mobile-uni run build:app 构建;它不生成签名 APK/IPA,也不适用本文件的 H5 rsync 发布流程。自定义基座、协议链接、签名和真机门禁见 MOBILE_APP_PACKAGING.md。

Qdrant 默认本地无需配置;远端或自定义 collection 可用 AIHR_QDRANT_URL、AIHR_QDRANT_COLLECTION、AIHR_QDRANT_API_KEY 覆盖。服务端资料导入根目录可用 AIHR_IMPORT_ROOT 或 -Daihr.import.root 覆盖。组织人员同步可用 AIHR_ORG_SYNC_BASE_URL 指向外部开放平台 /api/open/v1 前缀,并配置 AIHR_ORG_SYNC_ACCESS_TOKEN 或 AIHR_ORG_SYNC_CLIENT_ID/AIHR_ORG_SYNC_CLIENT_SECRET;业务请求会用 client secret 生成 HMAC-SHA256 hex 签名。移动端手机号登录的短信模板 ID、阿里云 AccessKey、Secret 和短信签名都通过环境变量注入;本地放根目录 .env.local,scripts/dev-backend.sh 会自动加载。若开放平台凭证放在 backend/.env,启动脚本也会加载该文件,并把 client_id/client_secret 映射为组织同步实际读取的 AIHR_ORG_SYNC_CLIENT_ID/AIHR_ORG_SYNC_CLIENT_SECRET。

组织同步写入前先运行只读预检:node scripts/verify-demo-questions.mjs --org-dry-run。dry-run 不检查或变更本地快照表结构,也不写数据库;输出只包含人数、手机号覆盖、脱敏数、疑似乱码数和警告,不输出员工姓名。2026-07-25 只读预检返回 3424 名员工、3398 个脱敏手机号和 1 条重复项目成员关系;当前上游优先字段 employee_number 仅匹配生产既有主体 7/3417,稳定 employee_id 匹配 3417/3417。全量快照仍存在脱敏手机号和重复关系,不得执行非 dry-run 请求,也不得用 allowPartialReplace=true 绕过。

员工岗位等字段的日常变化不要执行不安全的全量覆盖;部署支持增量同步的后端后,使用受 superadmin/hr_operator 保护的 POST /api/aihr/org/sync-changes。首次传 {"dryRun":true,"sinceTime":"YYYY-MM-DD HH:mm:ss"} 预检,再以相同 sinceTime 和 dryRun:false 写入;接口主动拉 /sync/changes 并按稳定 employee.id 回源详情,只替换命中的员工,脱敏手机号保留本地有效值,任职快照拉取失败则拒绝写库。

正式试点预检必须指定当前批次租户和时间窗,例如:AIHR_PILOT_TENANT_ID=000000 AIHR_PILOT_START_DATE=2026-07-07 AIHR_PILOT_END_DATE=2026-07-10 AIHR_PILOT_STRICT=true ./scripts/demo-check.sh。脚本只接受安全租户编号和 YYYY-MM-DD 日期,只统计目标租户窗口内完成的训练、校准和 SOP 评审;人员先按唯一手机号映射到在职组织快照,完训口径为每人至少 10 次已完成对练。

服务端正式试点 CSV 也必须显式指定同一批次日期;当前管理端不展示个人训练或试点导出入口。可用受角色保护的真实 API 验证其日期拒绝、CSV 契约和正式身份范围:node scripts/verify-demo-questions.mjs --pilot-export --start-date=2026-07-07 --end-date=2026-07-10。

本地 .env.local 示例:

AIHR_SMS_LOGIN_TEMPLATE_ID=SMS_xxxxxx
ALIYUN_SMS_ACCESS_KEY_ID=xxx
ALIYUN_SMS_ACCESS_KEY_SECRET=xxx
ALIYUN_SMS_SIGN_NAME=物业AI助手
# 演示兜底:非空则不真发短信,验证码固定为该值;dev 默认 123456
AIHR_SMS_DEV_FIXED_CODE=123456
# prod 默认 false;试点期仅在明确接受固定码风险时与上一项同时开启
AIHR_SMS_PROD_FIXED_CODE_ENABLED=false
AIHR_ORG_SYNC_BASE_URL=https://wuye.meihe.cc/api/open/v1
AIHR_ORG_SYNC_ACCESS_TOKEN=
AIHR_ORG_SYNC_CLIENT_ID=
AIHR_ORG_SYNC_CLIENT_SECRET=
AIHR_ORG_SYNC_SIGNING_SECRET=
# 若放在 backend/.env,也可沿用开放平台字段名:
# client_id=dn_xxxxxx
# client_secret=dns_xxxxxx
# 试点成本闸门:超预算时改 false,chat/asr/tts/vision 会停止外发并走现有兜底
AIHR_AI_RUNTIME_ENABLED=true
AIHR_AI_CHAT_ENABLED=true
AIHR_AI_SPEECH_ENABLED=true

全网检索访问密钥使用数据库表 aihr_web_ai_secret 中自动生成的主密钥加密,不依赖环境变量,也不通过通用参数接口回显。数据库迁移、备份和恢复必须同时保留该表;丢失或修改主密钥后需要重新保存提供方密钥。全网检索提供方只接受公网 HTTPS 地址,且每次修改地址或密钥后都必须重新连接测试,测试成功后才能启用。

application-dev.yml 只保留占位和默认值,不提交真实短信密钥。dev 环境不配阿里云短信也能登录移动端:验证码固定 123456。

Windows 10/11 + Docker Desktop

Windows 推荐让 Docker Desktop 承载 MySQL、Redis、MinIO 和 Qdrant,Java/Vite/portless 仍作为 Windows 进程运行;无需在 Windows 单独安装数据库或 Redis 服务。基础环境要求:

  • Docker Desktop 已启动,docker info 可连接 daemon
  • Git for Windows(使用其 Git Bash 和 OpenSSL)
  • JDK 17、Maven
  • Node.js 24+、npm、全局 portless
  • 可选:测试视频解析时另装 ffmpeg 和 ffprobe

1. PowerShell 会话准备

在仓库根目录打开 PowerShell。下面通过 git.exe 自动定位 Git for Windows,不依赖仓库或 Git 的固定安装盘符:

$gitRoot = Split-Path (Split-Path (Get-Command git).Source -Parent) -Parent
$gitBash = Join-Path $gitRoot 'bin\bash.exe'
$env:Path = "$(Join-Path $gitRoot 'usr\bin');$(Join-Path $gitRoot 'bin');$env:Path"

docker info
docker compose -f docker-compose.dev.yml config --quiet
java -version
mvn -version
node --version
portless --version
portless doctor

若 portless doctor 仅提示代理未启动,属于首启正常状态;若提示找不到 OpenSSL,确认本会话已执行上面的 $env:Path 设置。首次使用 HTTPS 本地域名时执行一次:

portless trust

公司代理、VPN 或 TUN 软件可能把 *.localhost 在 Node.js 中解析到 198.18.0.0/15,导致 Vite 页面能打开、/dev-api 却返回 500。下面的 Windows 流程先让 portless 为后端分配内部端口,再让两个 Vite 代理从 portless list 读取该端口并直连 127.0.0.1,因此不依赖 Node.js 对 *.localhost 的解析,也不要求 8080 空闲;浏览器入口仍使用 portless HTTPS 域名。若浏览器本身也无法解析这些域名,可在路由启动后,用管理员 PowerShell 执行一次 portless hosts sync。

2. 首次初始化

首次检出时安装两个当前前端工程的锁定依赖:

npm --prefix frontend ci
npm --prefix mobile-uni ci

首次创建本地测试库时,通过 Git Bash 执行初始化脚本:

& $gitBash ./scripts/reset-dev-db.sh

该脚本会在 Docker Desktop 中启动 wygj-mysql、wygj-redis、wygj-minio、wygj-qdrant,并删除后重建本地 ry-vue、导入若依与 AIHR schema/seed。仅首次初始化或明确需要清空本地测试数据时使用。

后续日常启动依赖容器不重建数据:

docker compose -f docker-compose.dev.yml up -d mysql redis qdrant minio minio-init

基础开发不要求 .env.local:dev 短信验证码默认固定为 123456,模型调用失败仍保留 seed fallback。真实短信、外部组织同步或外部 AI 模型联调时,再按前文示例创建被 Git 忽略的 .env.local。

3. 启动后端

新开一个 PowerShell,重复“PowerShell 会话准备”中的 $gitRoot、$gitBash 和 $env:Path 三行,然后执行:

portless run --name wygj-api --force bash ./scripts/dev-backend.sh

首次启动没有 backend/ruoyi-admin/target/ruoyi-admin.jar 时,脚本会先执行 Maven 打包;保持该终端运行。portless 分配的内部端口是动态的,对外入口始终是 https://wygj-api.localhost/。

4. 启动管理端与 mobile-uni

管理端和 mobile-uni 各使用一个 PowerShell,两个终端都先重复“PowerShell 会话准备”中的变量设置。

管理端终端在仓库根目录执行:

$apiRoute = (portless list | Select-String 'https://wygj-api\.localhost').Line
$apiPort = [regex]::Match($apiRoute, 'localhost:(\d+)').Groups[1].Value
if (-not $apiPort) { throw 'wygj-api portless route not found' }
$env:VITE_APP_PROXY_TARGET = "http://127.0.0.1:$apiPort"
& $gitBash ./scripts/dev-frontend.sh

mobile-uni 终端在仓库根目录执行:

$apiRoute = (portless list | Select-String 'https://wygj-api\.localhost').Line
$apiPort = [regex]::Match($apiRoute, 'localhost:(\d+)').Groups[1].Value
if (-not $apiPort) { throw 'wygj-api portless route not found' }
Set-Location .\mobile-uni
$env:VITE_PROXY_TARGET = "http://127.0.0.1:$apiPort"
portless run --name wygj-mobile-uni --force npm run dev

本机 portless 0.15.4 在没有根 package.json workspace 时不会仅凭 portless.json 批量启动两个前端,因此不要用根目录裸命令 portless 代替上述两个入口。后端重新启动后内部端口可能变化,此时也要重新启动两个前端以刷新代理目标。保持三个应用终端运行,访问:

  • 管理端:https://wygj-admin.localhost/
  • 用户侧:https://wygj-mobile-uni.localhost/h5/
  • 后端:https://wygj-api.localhost/

5. 冒烟与测试

$gitBash = Join-Path $gitRoot 'bin\bash.exe'

docker compose -f docker-compose.dev.yml ps
portless list

curl.exe -k -fsS https://wygj-admin.localhost/dev-api/auth/tenant/list
curl.exe -k -fsS https://wygj-admin.localhost/dev-api/auth/code
curl.exe -k -fsS https://wygj-mobile-uni.localhost/dev-api/api/aihr/mobile/home/user
docker exec wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 -N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='ry-vue';"
docker exec wygj-redis redis-cli -a ruoyi123 ping
curl.exe -fsS http://127.0.0.1:9000/minio/health/live
curl.exe -k -fsS http://127.0.0.1:6333/

npm --prefix mobile-uni run test:unit
npm --prefix mobile-uni run typecheck
npm --prefix frontend run build:dev
mvn -f backend/pom.xml -pl ruoyi-modules/ruoyi-aihr -am -DskipTests=false test
& $gitBash ./scripts/demo-check.sh

6. 停止

先在后端、管理端和 mobile-uni 三个终端按 Ctrl+C。需要同时停止代理和依赖容器时执行:

portless proxy stop
docker compose -f docker-compose.dev.yml stop

日常停止不需要 docker compose down -v;当前数据库等数据使用仓库 .data/ bind mount,只有明确重置时才运行 reset-dev-db.sh,不要手工删除 .data/mysql 等目录。

默认登录

  • 租户:000000
  • 管理员:admin / admin123
  • 测试账号:test / 666666、test1 / 666666

品牌资源

  • 页面标题:帮道
  • 侧栏 Logo:frontend/src/assets/logo/logo.png

已验证的基础链路

  • MySQL、Redis 与 MinIO 容器健康检查通过
  • Qdrant 容器随本地开发编排启动,供 SOP 知识库向量召回使用
  • ry_vue_5.X.sql、ry_job.sql、ry_workflow.sql 已导入
  • aihr_knowledge_mysql8.sql、aihr_model_mysql8.sql 已导入;本地库含住宅 SOP seed 片段与模型配置表
  • aihr_practice_mysql8.sql 已导入;移动端员工训练记录落 aihr_practice_session,用于训练历史、主管待复盘列表和能力画像聚合
  • aihr_interview_result_mysql8.sql 已纳入 reset 脚本;AI 面试评分完成后结果落 aihr_interview_result
  • aihr_candidate_material_mysql8.sql 已纳入;候选人端补充资料文件写 sys_oss/MinIO,关系落 aihr_candidate_material
  • aihr_org_snapshot_mysql8.sql 已纳入 reset 脚本;组织人员本地 seed(2 个住宅项目 22 人,项目经理/主管/一线三层)支撑演示,外部开放组织系统配置完成后用 POST /api/aihr/org/sync 拉取 company/department/employee 快照。2026-07-25 生产快照为 3417 名员工、2938 人在职、3392 个手机号映射;在职姓名已按稳定 employee_id 定向补齐,本次未做全量覆盖。
  • 源码 application-prod.yml 默认关闭 aihr.org-sync.store-display-fields;生产当前通过外部配置显式开启,用于主管团队、派发和考试对象显示已授权姓名。关闭该开关会让接口回退为编号/“员工”,修改前必须确认隐私口径与业务验收影响。项目范围、岗位和外部主体 ID仍用于权限与身份映射。
  • 本地组织 seed 带演示手机号;从 aihr_org_snapshot 按角色查询测试账号,分别验证员工端岗位识别和主管端项目范围,不在文档保存具体号码
  • 生产移动端回归同样由执行者从 aihr_org_snapshot 只读选择账号,不等待人工提供手机号:限定目标岗位、employment_status='active'、11 位 person_phone,并确认手机号与 ext_party_id 双向唯一。账号值只注入当前浏览器进程,不输出到终端、截图、报告或仓库。试点固定码开启时,登录顺序仍为先请求 /resource/sms/code、再提交固定码;直接提交固定码会得到 Captcha invalid,不应误判为账号或功能故障。
  • SOP 知识库支持 .txt/.md/.markdown/.pdf/.doc/.docx/.xls/.xlsx/.ppt/.pptx 上传到 MinIO 后解析入库,接口为 POST /api/knowledge/doc/upload,单文件上限 100MB;管理端上传请求单独放宽到 180s,PDF 解析/归类/向量化较慢时不要改全局 axios 超时。
  • 资料处理中心异步接口支持 .zip 和视频单文件 ≤500MB;生产 Spring/Undertow multipart 配置必须保持 max-file-size=500MB、max-request-size=520MB(源文件 backend/ruoyi-admin/src/main/resources/application-prod.yml,线上外置 /opt/wygj/config/application-prod.yml),修改后重启 wygj-aihr.service。非视频/ZIP 文件仍由队列限制为 100MB;ZIP 最多 1000 个子文件、解压总量 ≤2GB,不支持嵌套 ZIP。
  • 视频(.mp4/.mov/.avi/.mkv/.webm/.m4v,≤500MB、≤60 分钟)只走资料处理中心批量导入(异步队列):ffmpeg 抽音轨分段调 asr 转写 + 抽关键帧调 vision 提取画面文字,合并后归类切片入库,片段带 [mm:ss] 时间戳;依赖服务器安装 ffmpeg/ffprobe(macOS brew install ffmpeg,Linux apt install ffmpeg)
  • 服务端目录导入接口为 POST /api/knowledge/doc/import-local-task,只读取导入根目录下的相对目录,后台逐文件复用同一上传解析链路;POST /api/knowledge/doc/import-tasks/{id}/cancel 可取消运行中任务;POST /api/knowledge/doc/import-local 保留为同步调试接口。这组接口仅供运维/调试,不在资料处理页提供入口。
  • 启用 aihr_model_config.category='vector' 的模型配置后,上传会同步写入片段 embedding,并尽力 upsert 到 Qdrant;模型配置页可查看 Qdrant 维度、点数和片段向量数,并在维度不一致时重建索引;未配置或 Qdrant 不可用时只走 MySQL Fulltext/seed fallback,不影响检索。
  • 后端 ruoyi-admin 已完成 Maven 打包,并通过 portless 暴露为 https://wygj-api.localhost/
  • AIHR 真跑测试必须显式关闭父 POM 的默认跳测:mvn -f backend/pom.xml -pl ruoyi-modules/ruoyi-aihr -am -DskipTests=false test;普通 mvn ... test 只适合编译/打包检查。
  • 前端依赖已安装,Vite 已通过 portless 暴露为 https://wygj-admin.localhost/
  • 当前用户侧由 ./scripts/dev.sh 通过 portless 启动 mobile-uni,入口 https://wygj-mobile-uni.localhost/h5/;旧移动端兜底仅在 PORTLESS=0 调试时启动到 5174
  • 移动端手机号登录页已接 /resource/sms/code 与 /auth/mobile/sms-login;未配置真实 ALIYUN_SMS_ACCESS_KEY_ID、ALIYUN_SMS_ACCESS_KEY_SECRET、ALIYUN_SMS_SIGN_NAME、AIHR_SMS_LOGIN_TEMPLATE_ID 时不会发送阿里云短信;手机号不存在时会自动注册为 app_user
  • 移动端员工端“开始训练”已复用 /api/train/practice/start、/turn、/finish;移动端请求需带登录返回的 Authorization 与 clientid,服务端会强制当前 APP 身份和 mode=mobile,完成后才会改变主管端完训人数、“待复盘对练”计数、复盘列表、复盘详情、员工训练历史和能力画像。管理端训练看板的同一路径只允许 superadmin/hr_operator 做服务端绑定的 mode=preview 运营预览;预览不会冒用员工身份,也不计入上述统计、复盘或试点。
  • 移动端候选人端“开始面试/面试练习”已复用 /api/recruit/interview/start、/answer、/finish;“补充资料”需带移动端 Authorization 与 clientid,上传后写 sys_oss 和 aihr_candidate_material,管理端 /recruit/interview 可审核为 已通过/已驳回
  • /dev-api/auth/tenant/list 与 /dev-api/auth/code 已通过前端代理返回 200
  • 000000 / admin / admin123 已通过真实加密登录接口返回 access_token

Qdrant 单独检查:

curl -fsS http://127.0.0.1:6333/

登录后可检查向量索引状态:

TOKEN=<登录后 access_token>
curl -fsS https://wygj-api.localhost/api/knowledge/doc/vector-index-status -H "Authorization: Bearer $TOKEN"

线上管理端静态发布

线上根站由 Caddy 从服务器 /opt/wygj/www 提供;同一目录下的 /h5 是移动端静态资源。只改管理端静态资源时无需重启后端,但必须先备份整个目录,并且同步根目录时不能使用 --delete,以免删除移动端资源。

npm --prefix frontend run build:prod
ssh YCWY 'ts=$(date +%Y%m%d%H%M%S); mkdir -p /opt/wygj/backups; cp -a /opt/wygj/www /opt/wygj/backups/www-$ts; echo /opt/wygj/backups/www-$ts'
rsync -az frontend/dist/ YCWY:/opt/wygj/www/
curl -k -s https://peilian.njzhmj.top/ | sed -n '1,20p'

发布后以管理员真实会话回归受影响页面;如需核对指定资源,可比较本地构建文件和远端对应文件的 SHA-256。该流程不发布后端、不迁移数据库,也不覆盖 /opt/wygj/www/h5。

线上移动端静态发布

线上 peilian.njzhmj.top 由 Caddy 服务,/h5* 映射到服务器 /opt/wygj/www/h5,并 fallback 到 /h5/index.html。只改移动端静态资源时无需重启后端。

npm --prefix mobile-uni run build:h5
ssh YCWY 'ts=$(date +%Y%m%d%H%M%S); mkdir -p /opt/wygj/backups; cp -a /opt/wygj/www/h5 /opt/wygj/backups/h5-$ts; echo /opt/wygj/backups/h5-$ts'
rsync -az --delete mobile-uni/dist/build/h5/ YCWY:/opt/wygj/www/h5/
curl -k -s https://peilian.njzhmj.top/h5/ | sed -n '1,20p'

发布前先运行只读产物检查,记录 commit、管理端/H5/后端 hash;它不会连接 SSH、同步文件或重启服务:

./scripts/release-preflight.sh
RELEASE_REMOTE_URL=https://peilian.njzhmj.top ./scripts/release-preflight.sh
# 发布后核验线上主资源是否与当前本地产物一致
RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_MATCH=true ./scripts/release-preflight.sh
# 定向后端发布后只核对后端与 schema,不要求本轮未发布的管理端/H5 与本地一致
RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true ./scripts/release-preflight.sh
# 验证冻结提交生成的指定 JAR;提交必须是当前 HEAD 的祖先,且产物时间不得早于该提交
RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true RELEASE_ARTIFACT_COMMIT=<release-commit> RELEASE_LOCAL_BACKEND_PATH=<release-jar> ./scripts/release-preflight.sh
# 发布后同时核对线上静态资源、后端包和必需表(schema 检查为只读)
RELEASE_REMOTE_URL=https://peilian.njzhmj.top RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true ./scripts/release-preflight.sh

三个远端核验开关可以按实际发布单元独立组合,但只要启用任一开关就必须提供 RELEASE_REMOTE_URL。定向模式只要求对应的本地产物保持新鲜:静态核验要求管理端/H5,后端核验要求后端 JAR,单独 schema 核验不要求无关构建产物。默认以当前 HEAD 判断产物时间;冻结 RC 应同时设置 RELEASE_ARTIFACT_COMMIT 和 RELEASE_LOCAL_BACKEND_PATH,前者必须是当前 HEAD 的祖先,后者必须指向实际准备发布的 JAR,避免后续文档提交误伤冻结物或误用 target 下的其他构建。RELEASE_VERIFY_REMOTE_BACKEND=true 会打印整包 jar SHA-256 和 ruoyi-aihr 模块内容 SHA-256,实际匹配以模块内容 hash 为准;它不再隐式要求管理端/H5 静态资源匹配。只有同时启用 RELEASE_VERIFY_REMOTE_MATCH=true、RELEASE_VERIFY_REMOTE_BACKEND=true 和 RELEASE_VERIFY_REMOTE_SCHEMA=true,才能称为完整包匹配。远端后端默认核对 /opt/wygj/app/ruoyi-admin.jar,如发布路径不同可通过 RELEASE_REMOTE_BACKEND_PATH 覆盖;路径必须是安全的绝对路径。开启远端后端或 schema 核验时,预检还会只读检查 wygj-aihr.service 及其 EnvironmentFile 的有效配置,AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP 必须为 false 或未设置;无法读取引用的环境文件会失败关闭,而不会把本机环境变量当作线上证据。

Android 人工验收每完成一个步骤,用 .\scripts\capture-android-acceptance.ps1 -Serial <adb-serial> -Stage <stage> 保存当前状态;脚本只读取包版本、前台 Activity、权限、UI、截图和退出记录,不执行安装、启动、清数据、授权或点击。证据仅写入已忽略的 output/aug1-rc/android-acceptance/,详细口径见 docs/MOBILE_APP_DEVICE_ACCEPTANCE.md。

带 RELEASE_REMOTE_URL 时,预检同时校验租户接口 JSON 的业务 code=200;HTTP 200 但业务返回 401/405 会判定失败。

发布时必须保留 preflight 输出、远端备份目录和发布后浏览器回归结果;回滚优先使用对应备份目录恢复,再重启后端服务,不能直接覆盖当前线上目录而不留证据。

定向后端发布与回滚

8 月 1 日独立 APK 如只需补齐候选后端语义,使用固定拓扑的 release-backend.sh,不要手工 scp 后直接覆盖。脚本只允许 YCWY:/opt/wygj/app/ruoyi-admin.jar 和 wygj-aihr.service,默认 plan 与 rollback-plan 只读;deploy/rollback 必须使用只读计划输出的完整 SHA-256 授权串并保持 Git 工作区干净。计划阶段会核对冻结提交、JAR 哈希与 AIHR 模块、生产目标/服务/空间和 schema;部署阶段先保留旧 JAR 与清单,再在同目录校验并原子切换、重启、执行后端 + schema 预检,任何激活或发布后预检失败都会尝试恢复旧 JAR。回滚前还会额外备份当时正在运行的 JAR。脚本不删除备份,不发布管理端/H5,不执行数据库迁移。

先只读生成计划:

./scripts/release-backend.sh plan \
  --artifact output/aug1-release/0.1.10-110-dc20b920/ruoyi-admin-dc20b920.jar \
  --artifact-commit dc20b92061f10a356ed73eb105ef7b00c8b8a617 \
  --expected-sha256 8c676f2fbfccb4f4573cbca1259e8b26eb83ebdb469adcbaf714c5931ef3cf2d

只有负责人明确授权后,才能把计划输出的 approval_token 原样传给 deploy;仅运行 plan 不构成发布授权:

./scripts/release-backend.sh deploy \
  --artifact output/aug1-release/0.1.10-110-dc20b920/ruoyi-admin-dc20b920.jar \
  --artifact-commit dc20b92061f10a356ed73eb105ef7b00c8b8a617 \
  --expected-sha256 8c676f2fbfccb4f4573cbca1259e8b26eb83ebdb469adcbaf714c5931ef3cf2d \
  --approval 'DEPLOY_BACKEND:<完整候选JAR-SHA256>'

发布成功会输出 rollback_backup、rollback_sha256 和回滚授权串。先只读核对备份,再在获得单独回滚授权后执行:

./scripts/release-backend.sh rollback-plan \
  --backup-dir '<deploy输出的rollback_backup>' \
  --expected-sha256 '<deploy输出的rollback_sha256>'

./scripts/release-backend.sh rollback \
  --backup-dir '<deploy输出的rollback_backup>' \
  --expected-sha256 '<deploy输出的rollback_sha256>' \
  --approval 'ROLLBACK_BACKEND:<完整备份JAR-SHA256>'

发布前后均可重复运行下列无登录安全探针。它只对 APK 依赖的 20 个入口发送 GET:1 个公开首页必须返回业务 200,6 个鉴权 GET 必须返回 401,13 个仅允许 POST 的入口用 GET 必须返回 405。/api/knowledge/answer-feedback 同时存在员工 POST 和运营 GET,因此归入鉴权 GET。脚本明确排除短信验证码入口,不登录、不上传、不提交业务请求;通过只证明路由存在且无登录访问失败关闭,不能替代认证态业务验收:

./scripts/verify-aug1-production-api-readonly.sh

冻结运行时产物后,如发布/回滚或验收工具继续修正,不要重编同版本 APK/JAR 冒充新运行时。当前打包器默认使用 b9e08789 的 0.1.12 (112) 统一 RC 证据,并把当前干净 HEAD 作为 operations commit;新包名同时包含两者短哈希,旧 ZIP 不覆盖:

.\scripts\package-aug1-release.ps1 `
  -BackendJar .\backend\ruoyi-admin\target\ruoyi-admin.jar

打包器重新校验冻结证据、APK/JAR/内容哈希和提交祖先关系,按固定顺序与时间戳生成 14 个文件的 ZIP,并附带发布、预检、API 探针、Android 取证及最新 Go/No-Go 的审计快照。ZIP 内 operations 脚本用于交付审计;实际执行仍须从对应 operations commit 的干净仓库运行。

MVP 页面验证

登录后侧栏应只展示以下入口:

  • 首页:/index
  • AI面试:/recruit/interview
  • 三角色对练:/train/practice
  • 案例沉淀:/knowledge/cases
  • SOP知识库:/knowledge/sop
  • 资料处理:/knowledge/processing
  • 系统设置-模型配置:/system/model

若依默认菜单如“系统管理 / 租户管理 / 系统监控 / 系统工具 / 测试菜单”在当前 MVP 阶段应保持隐藏。

MVP 演示流验证

当前已跑通管理端五个本地演示流和移动端员工训练闭环:

  • AI面试:进入 /recruit/interview,点击“生成题目” → 输入真实回答或“填满参考回答” → “完成评分”,应看到新增面试记录;配置 chat 模型时题目和评分都来自真实模型,未配置时使用本地 Rubric。
  • 三角色对练:进入 /train/practice,点击“开始对练” → 完成两轮真实或参考回复 → “结束并评分”,应看到“已完成闭环”“导师改写”和新增对练记录。
  • 案例沉淀:进入 /knowledge/cases,上传真实语音 → “AI 整理” → “送审” → “入库”,应看到“已完成闭环”和新增案例记录;上传必须先完成 ASR 转写。
  • SOP知识库:进入 /knowledge/sop,可上传 txt/md/PDF/Word/Excel/PPT 文档入库;点击“检索” → “生成训练题”,应看到“已完成闭环”、命中数据库 SOP 原文片段和训练题;数据库不可用时页面回退 seed。
  • 资料处理:进入 /knowledge/processing,应看到资料总量、解析任务表、处理链路、规则与风险;页面只保留“批量导入”和“刷新”,不应出现“选择目录”“服务端导入”或目录导入任务面板。批量导入走异步队列:提交即返回,页面出现“本次批量上传”进度面板(排队/加工中/完成/失败 + 单条重试),后台 worker 并发 2 逐条解析入库;支持多文件和 ZIP,暂存目录默认 ./.data/staging(aihr.upload.staging 覆盖)。
  • uni-app 员工端:进入 https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index,手机号登录(dev 验证码固定 123456)。今日页进入问师傅、练习、案例素材和个人页;练习页应能完成开始练习、提交回应、结束评分、每日三题提交,个人页同步训练历史和成长证据包。每日三题正式按组织快照 hire_date 判断入职三个月窗口;本地 Demo 若快照尚无该字段,仅由 dev/local profile 且 application-dev.yml 的 aihr.practice.allow-legacy-daily-drill-fallback=true 启用训练次数回退,生产 profile 即使误传环境变量也强制关闭。
  • 工作助手确认式统一采集:/h5/#/pages/user/sop/index 一级入口只保留“工作助手 / 查全网”,既有文字、ASR、媒体、数据工具和 30 分钟/最近 6 轮短会话链保持不变;显式“记一下/帮我记/保存一下”返回 DRAFT 确认卡,位置缺失只作可选提示。确认后写 aihr_assistant_capture:PRIVATE/NOT_REQUIRED 仅本人可见,COMPANY/PENDING 只表示待流转;旧项目记录继续使用 aihr_service_memory/version。/h5/#/pages/user/assistant/memories 展示待确认候选和本人已确认记录;不依赖新的 Qdrant collection,提醒默认关闭。
  • 上一条只验证当前已部署的确认式采集;项目名称下拉、项目化会话、原始来源绑定、今日工作成果和主管按日汇总仍属后续迭代,不得在本地回归结果中写成已完成。
  • 移动端登录后若组织快照接口返回 401/403,只降级为手动岗位确认,不应清除手机号登录态;岗位确认页仍需允许员工选择“生活顾问”后继续进入业务页。
  • 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 代替浏览器文件选择。
  • 真 LLM 激活:在 /system/model 给供应商填 api_host/api_key 并启用 category=chat 模型后,三角色对练的客户回复与评分即为真实 LLM 生成;再启用 category=asr/tts 语音路径生效。阿里云短音频 ASR 使用 provider_code=dashscope、model_name=qwen3-asr-flash 和北京工作空间的 /compatible-mode/v1 Base URL;现有 backend/.env 中同一工作空间的 AIHR_QWEN_REALTIME_ENDPOINT / AIHR_QWEN_REALTIME_API_KEY 可作为本地模型管理的配置来源,但应用不会自动把私密变量导入模型表。密钥只通过模型管理或本地私密配置提供,不写入文档、SQL 或受版本控制配置。SiliconFlow SenseVoice/CosyVoice2 仍作为兼容供应商。未配置时全链路自动回退 seed。

演示前可先跑最小预检:

./scripts/demo-check.sh
bash scripts/tests/aihr-schema-migrations.test.sh
node --test mobile-uni/tests/personal-assistant.test.mjs
API_BASE=https://wygj-api.localhost ./scripts/personal-assistant-smoke.sh
# 登录后只读核验:TOKEN=<mobile-access-token> API_BASE=https://wygj-api.localhost ./scripts/personal-assistant-smoke.sh

工作助手记忆本地人工回归至少覆盖:登录后说“帮我记一下三栋 3203 需要保洁服务”,即使缺少单元也应看到 DRAFT 卡;“暂不保存”不产生正式记录;“仅自己保存”后只由本人召回;“提交公司处理”只显示 PENDING,不得显示已送达;同一会话重复描述更新原卡,“另外记一条”才新建;双击确认不得重复写入。再用另一账号验证个人记录不可见,并验证按精确房号查询不会混入同楼栋其他房号。

完整演示脚本与录屏兜底见 DEMO_ACCEPTANCE.md。

个人 AI 助理本地验证

个人知识使用独立 Qdrant collection,默认 aihr_personal_knowledge,payload 强制包含 tenant_id、owner_user_id、item_id 与 captured_at。对象存储固定使用 personal-minio 配置和私有 bucket ruoyi-personal,不修改企业资料使用的默认 minio/ruoyi。MySQL 表和专用 OSS 配置由 backend/script/sql/aihr_personal_knowledge_mysql8.sql 初始化;旧开发库先运行 COMPOSE_PROJECT_NAME=wygj ./scripts/reset-dev-db.sh。

企业范围问答另外依赖 aihr_org_snapshot 与 aihr_knowledge_acl。reset 会给企业 SOP 1001/1002/1003 写入“一线生活顾问”POSITION ACL;本地物业管家 seed 账号由服务端按固定别名映射到生活顾问后获得 allowlist。普通新注册手机号没有组织快照,企业范围必须返回无权限。不要用手工手机号、客户端岗位参数或 TENANT ACL 绕过该默认拒绝;开放组织系统只负责刷新阶段二快照,北森实时组织/任职仍属阶段三。

空间与处理配额通过 aihr.personal.* 配置覆盖,包括空间字节配额、资料数量、单文件大小、抓取字节/超时、worker 与 cleanup 批量大小。不要把个人 collection 改回企业 aihr_knowledge。

启动依赖与后端后运行真实隔离 smoke:

./scripts/dev.sh
./scripts/personal-assistant-smoke.sh

脚本每次生成唯一 smoke 手机号与 run marker,通过开发短信登录创建 A/B,并真实采集 TEXT、由 macOS cupsfilter 生成的可检索 PDF、公开网页 https://example.com/。它会验证三类资料 READY/检索/引用、owner 隔离、私有 OSS 匿名 403、SSRF、幂等删除,以及 MySQL/MinIO/Qdrant 零残留;退出时按本次临时用户清理其全部个人会话/消息,并按 user/item/OSS/job ID 与 run marker 回查清理。公开网页可用 AIHR_PERSONAL_SMOKE_PUBLIC_URL 覆盖,页面检索词可用 AIHR_PERSONAL_SMOKE_PUBLIC_QUERY 覆盖;客户端只检查 URL 语法,不预先跟随重定向,DNS、逐跳 SSRF 校验和最终 READY 状态以后端为准,公网不可达会明确失败。需要为重定向目标做精确断言时可设置 AIHR_PERSONAL_SMOKE_EXPECTED_PUBLIC_URL。脚本不会输出 token。./scripts/personal-assistant-smoke.sh --signal-self-test 可单独验证 INT/TERM 分别返回 130/143。

个人网页采集默认从 /etc/resolv.conf 读取最多 4 个 DNS resolver,并使用有 socket deadline 的原生 UDP 查询;如运行环境的 resolver 配置不可用,可通过 AIHR_PERSONAL_DNS_SERVERS=223.5.5.5,1.1.1.1 显式覆盖。配置项只接受数字 IP,不会递归解析 DNS 服务器名称。