feat(aihr): finalize practice growth and access boundaries

This commit is contained in:
2026-07-25 00:42:19 +08:00
parent a90f40e4d8
commit c9d2012ec1
87 changed files with 6804 additions and 2538 deletions
+27 -33
View File
@@ -43,7 +43,7 @@ check_mobile_h5_shell() {
icon_url="${MOBILE_URL%/}${href}"
code="$(curl -k -sS -o /tmp/wygj-demo-check.favicon -w '%{http_code}' "$icon_url")"
[[ "$code" == "200" ]] || fail "mobile H5 favicon returned HTTP $code: $icon_url"
contains mobile-uni/src/manifest.json '"enable": false'
contains mobile-uni/src/manifest.json '"enable" : false'
echo "OK: mobile H5 shell"
}
@@ -417,7 +417,7 @@ contains mobile-uni/src/services/home.ts "/api/aihr/mobile/home/"
contains mobile-uni/src/services/home.ts "auth: isLoggedIn()"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java "public HomeResponse publicHome(String role)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "return R.ok(mobileSeedService.publicHome(role))"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "mobileSeedService.home(role, identity)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "mobileSeedService.home(role, resolvedIdentity)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "!UserType.APP_USER.getUserType().equals(loginUser.getUserType())"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "无权访问该录音"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "return AihrSensitiveText.forModel(value)"
@@ -462,7 +462,6 @@ contains mobile-uni/src/pages/candidate/materials/index.vue "clearData();"
contains mobile-uni/src/pages/candidate/progress/index.vue "const clearData = () =>"
contains mobile-uni/src/pages/candidate/study/index.vue "searchKnowledge"
contains mobile-uni/src/pages/user/learning/index.vue "岗位 SOP"
contains mobile-uni/src/pages/user/learning/index.vue "当前摘要不代表正式上岗资格"
contains mobile-uni/src/pages/user/learning/index.vue "岗位学习范围加载失败"
contains mobile-uni/src/pages/user/learning/index.vue "getPracticeAssignments"
contains mobile-uni/src/pages/user/learning/index.vue "searchKnowledge"
@@ -471,8 +470,6 @@ contains frontend/src/views/recruit/interview.vue "建议复试"
contains frontend/src/views/recruit/interview.vue 'v-loading="recordsLoading"'
contains frontend/src/views/recruit/interview.vue "const recordsLoading = ref(true)"
contains frontend/src/views/train/practice.vue "导师改写"
contains frontend/src/views/train/practice.vue 'v-loading="recordsLoading"'
contains frontend/src/views/train/practice.vue "const records = ref<Array<(typeof baseRecords)[number]>>([])"
contains frontend/src/views/knowledge/cases.vue "真实语音"
contains frontend/src/views/knowledge/cases.vue 'v-loading="recordsLoading"'
contains frontend/src/views/knowledge/cases.vue "const records = ref<CaseRecord[]>([])"
@@ -485,11 +482,8 @@ contains backend/script/sql/update/aihr_20260714_case_learning_mysql8.sql "super
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java "/records/{caseId}/review"
contains frontend/src/api/aihr/case.ts "reviewKnowledgeCase"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "系统用户上传案例必须明确所属项目"
contains frontend/src/views/index.vue "核心流程覆盖待内容确认"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrDashboardService.java "核心流程覆盖待内容确认"
contains frontend/src/views/index.vue "不等于端到端验收"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrDashboardService.java "不等于端到端验收"
contains frontend/src/views/index.vue "正式试点身份待核对"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrDashboardService.java "正式试点身份待核对"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "TenantHelper.getTenantId()"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java "TenantHelper.getTenantId()"
@@ -500,7 +494,6 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "private String persistedTraineeName"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "persistedTraineeName(extPartyId.matches"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "if (!orgSnapshotTableExists() && !isDemoProfile())"
contains frontend/src/views/index.vue "待同步"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrDashboardService.java "核心流程待确认"
contains frontend/src/views/knowledge/sop.vue "训练题已生成"
contains frontend/src/views/knowledge/sop.vue "不代表这些文档已真实入库"
@@ -515,14 +508,12 @@ contains frontend/src/views/system/model/index.vue "提示词模板"
contains frontend/src/views/system/model/index.vue "不代表真实租户配置"
contains frontend/src/views/system/model/index.vue "不代表真实租户模板数据"
contains frontend/src/views/system/prompts.vue "保存并启用"
contains frontend/src/views/competency/growth.vue "学分规则(待 HR 配置)"
contains frontend/src/views/competency/growth.vue "规划参考,当前不生效"
contains frontend/src/views/competency/growth.vue "训练画像等级(参考)"
contains frontend/src/views/competency/growth.vue "人工贡献分"
contains frontend/src/views/train/reviews.vue "人工贡献分"
contains frontend/src/views/train/reviews.vue "incentivePoint: null as number | null"
contains frontend/src/views/train/reviews.vue "incentivePoint: reviewForm.incentivePoint ?? undefined"
contains frontend/src/views/competency/growth.vue "person.personName || person.extPartyId"
contains frontend/src/router/index.ts "path: 'reviews'"
contains frontend/src/router/index.ts "path: 'difficulties'"
contains frontend/src/router/index.ts "path: '/competency/growth'"
contains frontend/src/router/index.ts "redirect: '/train/tasks'"
contains mobile-uni/src/pages/supervisor/review/index.vue "人工贡献分"
contains mobile-uni/src/pages/supervisor/review/index.vue "confirmedGrowthLevel"
contains mobile-uni/src/pages/user/profile/index.vue "成长证据包(待人工复核)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "训练画像参考等级"
contains frontend/src/router/index.ts "/system/prompts"
@@ -545,8 +536,9 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service
contains frontend/src/api/aihr/practice.ts "/api/train/practice/start"
contains frontend/src/api/aihr/practice.ts "/api/aihr/prompt-templates"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java "/api/train/practice"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java "practiceSeedService.turn(request, currentAppUsername(), currentAppUserId())"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java "practiceSeedService.finish(request, currentAppUsername())"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java 'practiceSeedService.turn(request, requirePracticeActorIdentity("继续训练"), currentAppUserId())'
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java 'practiceSeedService.finish(request, requirePracticeActorIdentity("完成训练"))'
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrPracticeController.java "private static String requirePracticeActorIdentity(String action)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "训练会话不存在或无权访问"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "finished_time IS NOT NULL"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "人工校准必须明确填写总分"
@@ -575,12 +567,13 @@ contains backend/script/sql/aihr_practice_mysql8.sql "task_completion"
contains backend/script/sql/update/aihr_20260714_practice_five_dimensions_mysql8.sql "aihr_brd_rubric_codes"
contains backend/script/sql/update/aihr_20260715_practice_session_evidence_mysql8.sql "annotations_json"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/domain/AihrPracticeDto.java "MistakeAggregateResponse"
contains mobile-uni/src/pages/supervisor/team/index.vue "团队错题本"
contains mobile-uni/src/pages/supervisor/team/index.vue "常见难题"
contains mobile-uni/src/pages/supervisor/team/index.vue "一键派专项"
contains mobile-uni/src/types/api.ts "PracticeMistakeAggregate"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "/practice/mistakes"
contains mobile-uni/src/pages/user/learning/index.vue "错题本"
contains mobile-uni/src/pages/user/learning/index.vue "mistakeFilters"
contains mobile-uni/src/pages/user/profile/index.vue "AI训练综合分仅作训练参考"
contains mobile-uni/src/pages/user/profile/index.vue "AI训练综合分仅作参考"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "aihr_practice_session"
contains backend/script/sql/aihr_practice_mysql8.sql "aihr_practice_scenario"
contains backend/script/sql/update/aihr_20260714_practice_catalog_schema_mysql8.sql "aihr_practice_rubric_dimension"
@@ -596,12 +589,9 @@ contains backend/script/sql/aihr_practice_mysql8.sql "summary_card"
contains frontend/src/views/train/practice.vue "训练前预习卡"
contains frontend/src/views/train/practice.vue "Rubric 预览"
contains frontend/src/views/train/practice.vue "提示词模板"
contains frontend/src/views/train/practice.vue "导出试点CSV"
contains frontend/src/views/train/practice.vue "训练记录接口暂不可用,当前显示演示记录"
contains frontend/src/views/train/practice.vue "listPracticeReviews"
contains frontend/src/views/train/reviews.vue "导出试点 CSV"
contains frontend/src/views/train/reviews.vue "人工校准总分"
contains frontend/src/views/train/reviews.vue "不展示演示复盘记录或猜测性试点指标"
contains frontend/src/views/train/practice.vue "此页仅用于不绑定员工身份的运营预览"
contains frontend/src/views/train/practice.vue "不会使用或生成员工身份"
contains frontend/src/router/index.ts "meta: { title: '复盘校准', icon: 'finish', activeMenu: '/train/tasks' }"
contains frontend/src/api/aihr/practice.ts "/api/train/practice/export"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "final_adopted_score"
contains backend/script/sql/aihr_practice_mysql8.sql "aihr_practice_calibration"
@@ -640,7 +630,7 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/control
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java 'firstNonBlank(position, "生活顾问")'
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "formalPositionSopTopics(position, effectiveProjectType, projectCodes)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "formal-applicability"
contains mobile-uni/src/pages/user/learning/index.vue "qualificationStatus === 'NOT_CONFIGURED'"
contains mobile-uni/src/pages/user/learning/index.vue "const qualificationStatusLabel = computed(() =>"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java "currentAppProjectCodes()"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "FIND_IN_SET(?, REPLACE(a.org_scope, ' ', '')) > 0"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "normalizeSearchSource"
@@ -668,7 +658,9 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSpeechService.java "Duration.ofSeconds(5)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrMobileSeedService.java "待复盘对练"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "/practice/reviews"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "StpUtil.hasRoleOr(TenantConstants.SUPER_ADMIN_ROLE_KEY, \"hr_operator\")"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "private String supervisorScopeExtPartyId()"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "后台管理账号不能使用主管带教接口"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "mobileSeedService.requireMobileSupervisorIdentity(currentAppUsername())"
contains mobile-uni/src/pages/supervisor/review/index.vue "人工贡献分"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "/practice/reviews/{id}/reviewed"
contains mobile-uni/src/pages/supervisor/review/index.vue "标记已复盘"
@@ -712,7 +704,8 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java "currentProjectScopes"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrCaseService.java "projectScopesForAppUser"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrCaseController.java "/api/knowledge/case"
contains frontend/src/api/aihr/sop.ts "/api/knowledge/search"
contains frontend/src/api/aihr/sop.ts "/api/knowledge/query"
contains frontend/src/api/aihr/sop.ts "UnifiedQueryResponse"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java "@SaCheckLogin"
contains frontend/src/api/aihr/sop.ts "/api/knowledge/doc/upload"
contains frontend/src/api/aihr/sop.ts "/api/knowledge/reviews"
@@ -740,7 +733,8 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java "requireSessionTenant(session, tenantId())"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrInterviewService.java "private record InterviewSession(String tenantId"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "canReadPracticeAudioForTenant"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "mobileSeedService.canReadPracticeAudioForTenant(ossId)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "if (!UserType.APP_USER.getUserType().equals(loginUser.getUserType()))"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrMobileController.java "mobileSeedService.canReadPracticeAudio(ossId, currentAppUsername())"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java "oss.getOssId(), \"\")"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java "new TtsResponse(inlineAudioUrl, \"openai-compatible\", oss.getOssId(), inlineAudioUrl)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrPracticeSeedService.java "exposedAudioUrl"
@@ -755,7 +749,8 @@ contains backend/ruoyi-admin/src/main/resources/application-dev.yml "AIHR_REDIS_
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrOrgSyncService.java "person_phone like ?"
contains backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/service/AihrOrgSyncServiceTest.java "hiddenDisplayFieldsStillAllowIdentitySearchByPhone"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "requester_ext_party_id"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSopController.java "currentRequesterExtPartyId"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/service/AihrKnowledgePrincipalResolver.java "mobileSeedService.requireMobileIdentity(trim(loginUser.getUsername()))"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/knowledge/service/AihrKnowledgeQueryService.java 'principal == null ? "" : principal.extPartyId(), spaceIds'
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "hasExplicitSopReviewDecision"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "validateFeedbackFragmentOwnership"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSopSeedService.java "normalizeFeedbackReviewId"
@@ -768,7 +763,6 @@ contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/controller/AihrSpeechController.java "tts audio upload failed(处理错误已隐藏)"
contains mobile-uni/src/pages/user/history/index.vue "inlineAudioSrc"
contains mobile-uni/src/pages/supervisor/review/index.vue "inlineAudioSrc"
contains frontend/src/views/train/reviews.vue "inlineAudioSource"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrSensitiveText.java "外部 AI 模型前"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java "AihrSensitiveText.forModel(prompt)"
contains backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/service/AihrModelSeedService.java "外部响应体已隐藏"
+11 -2
View File
@@ -244,12 +244,21 @@ else
fi
PIDS+=("$!")
./scripts/dev-frontend.sh &
BACKEND_PROXY_TARGET=""
if using_portless_backend; then
for _ in {1..20}; do
BACKEND_PROXY_TARGET="$(portless get wygj-api 2>/dev/null || true)"
[[ -n "$BACKEND_PROXY_TARGET" ]] && break
sleep 0.1
done
fi
VITE_APP_PROXY_TARGET="${VITE_APP_PROXY_TARGET:-$BACKEND_PROXY_TARGET}" ./scripts/dev-frontend.sh &
PIDS+=("$!")
if using_portless_mobile; then
portless alias --remove wygj-mobile-uni >/dev/null 2>&1 || true
VITE_PROXY_TARGET="${VITE_PROXY_TARGET:-https://wygj-api.localhost}" portless run --name wygj-mobile-uni --force npm --prefix mobile-uni run dev &
VITE_PROXY_TARGET="${VITE_PROXY_TARGET:-$BACKEND_PROXY_TARGET}" portless run --name wygj-mobile-uni --force npm --prefix mobile-uni run dev &
else
./scripts/dev-mobile.sh &
fi
+143
View File
@@ -71,6 +71,9 @@ fi
if [[ "${RELEASE_VERIFY_REMOTE_SCHEMA:-false}" == "true" && "${RELEASE_VERIFY_REMOTE_MATCH:-false}" != "true" ]]; then
fail "RELEASE_VERIFY_REMOTE_SCHEMA=true requires RELEASE_VERIFY_REMOTE_MATCH=true"
fi
if [[ "${AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP:-false}" == "true" ]]; then
fail "AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP must stay false for a release; apply the formal SQL migration instead"
fi
head_epoch="$(git show -s --format=%ct HEAD)"
@@ -106,6 +109,60 @@ require_remote_business_success() {
echo "$label=200 $url"
}
require_remote_practice_schema_guard() {
local remote_ssh="${RELEASE_REMOTE_SSH:-YCWY}"
local remote_service="${RELEASE_REMOTE_SERVICE:-wygj-aihr.service}"
[[ "$remote_service" =~ ^[A-Za-z0-9_.@-]+\.service$ ]] \
|| fail "remote systemd service name is invalid: $remote_service"
local state
state="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_service" <<'REMOTE'
set -euo pipefail
service="$1"
matches_enabled_guard() {
grep -Eiq '(AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP|aihr\.practice\.runtime-schema-bootstrap)[[:space:]]*=[[:space:]]*"?true"?'
}
runtime_environment="$(systemctl show "$service" --property=Environment --value --no-pager)"
unit_source="$(systemctl cat "$service")"
if printf '%s\n' "$runtime_environment" | matches_enabled_guard \
|| printf '%s\n' "$unit_source" | matches_enabled_guard; then
printf 'enabled\n'
exit 0
fi
while IFS= read -r environment_file; do
[[ -z "$environment_file" ]] && continue
if [[ ! -r "$environment_file" ]]; then
printf 'unreadable:%s\n' "$environment_file"
exit 0
fi
if matches_enabled_guard < "$environment_file"; then
printf 'enabled\n'
exit 0
fi
done < <(printf '%s\n' "$unit_source" | sed -nE 's/^[[:space:]]*EnvironmentFile=-?([^[:space:]#]+).*/\1/p')
printf 'disabled-or-default\n'
REMOTE
)" || fail "remote runtime schema guard check failed: $remote_ssh:$remote_service"
case "$state" in
disabled-or-default)
echo "remote_practice_runtime_schema_bootstrap=false/default $remote_ssh:$remote_service"
;;
enabled)
fail "remote AIHR_PRACTICE_RUNTIME_SCHEMA_BOOTSTRAP is enabled; disable it and apply formal SQL migrations"
;;
unreadable:*)
fail "remote runtime schema guard cannot inspect ${state#unreadable:}; grant read access or unset the file before release"
;;
*)
fail "remote runtime schema guard returned an unexpected state: $state"
;;
esac
}
require_remote_schema() {
local remote_ssh="${RELEASE_REMOTE_SSH:-YCWY}"
local remote_db="${RELEASE_REMOTE_DB_NAME:-ry-vue}"
@@ -232,6 +289,87 @@ REMOTE
[[ -z "$missing_work_report_columns" ]] \
|| fail "remote work-report schema missing required columns: $(printf '%s' "$missing_work_report_columns" | tr '\n' ', ' | sed 's/, $//')"
local missing_practice_curriculum_columns
missing_practice_curriculum_columns="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE'
set -euo pipefail
db="$1"
mysql --batch --skip-column-names --connect-timeout=5 "$db" <<'SQL'
SELECT CONCAT(required.table_name, '.', required.column_name)
FROM (
SELECT 'aihr_practice_scenario' AS table_name, 'growth_level' AS column_name
UNION ALL SELECT 'aihr_practice_scenario', 'competency_code'
UNION ALL SELECT 'aihr_practice_scenario', 'collaboration_positions'
UNION ALL SELECT 'aihr_practice_scenario', 'review_status'
UNION ALL SELECT 'aihr_practice_scenario', 'risk_level'
UNION ALL SELECT 'aihr_practice_scenario', 'curriculum_version'
UNION ALL SELECT 'aihr_practice_scenario', 'reviewer'
UNION ALL SELECT 'aihr_practice_scenario', 'reviewer_user_id'
UNION ALL SELECT 'aihr_practice_scenario', 'reviewed_time'
UNION ALL SELECT 'aihr_practice_scenario', 'second_reviewer'
UNION ALL SELECT 'aihr_practice_scenario', 'second_reviewer_user_id'
UNION ALL SELECT 'aihr_practice_scenario', 'second_reviewed_time'
UNION ALL SELECT 'aihr_practice_session', 'position_snapshot'
UNION ALL SELECT 'aihr_practice_session', 'project_type_snapshot'
UNION ALL SELECT 'aihr_practice_session', 'growth_level'
UNION ALL SELECT 'aihr_practice_session', 'competency_code'
UNION ALL SELECT 'aihr_practice_session', 'collaboration_positions'
UNION ALL SELECT 'aihr_practice_session', 'curriculum_version'
UNION ALL SELECT 'aihr_practice_session', 'growth_confirmation_level'
UNION ALL SELECT 'aihr_practice_session', 'growth_confirmed_by'
UNION ALL SELECT 'aihr_practice_session', 'growth_confirmed_time'
) required
LEFT JOIN information_schema.columns actual
ON actual.table_schema = DATABASE()
AND actual.table_name = required.table_name
AND actual.column_name = required.column_name
WHERE actual.column_name IS NULL
ORDER BY required.table_name, required.column_name;
SQL
REMOTE
)" || fail "remote practice-curriculum schema check failed: $remote_ssh:$remote_db"
[[ -z "$missing_practice_curriculum_columns" ]] \
|| fail "remote practice-curriculum schema missing required columns: $(printf '%s' "$missing_practice_curriculum_columns" | tr '\n' ', ' | sed 's/, $//')"
local practice_assignment_request_unique_index
practice_assignment_request_unique_index="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE'
set -euo pipefail
db="$1"
mysql --batch --skip-column-names --connect-timeout=5 "$db" <<'SQL'
SELECT CONCAT(
COALESCE(MIN(non_unique), -1), '|',
COALESCE(GROUP_CONCAT(column_name ORDER BY seq_in_index), ''), '|',
COALESCE(SUM(sub_part IS NOT NULL), 0)
)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'aihr_practice_assignment'
AND index_name = 'uk_aihr_assignment_request';
SQL
REMOTE
)" || fail "remote practice-assignment idempotency-index check failed: $remote_ssh:$remote_db"
[[ "$practice_assignment_request_unique_index" = "0|tenant_id,request_key,ext_party_id|0" ]] \
|| fail "remote practice-assignment idempotency index invalid: expected unique full columns tenant_id,request_key,ext_party_id; got ${practice_assignment_request_unique_index:-missing}"
local practice_assignment_recent_content_index
practice_assignment_recent_content_index="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE'
set -euo pipefail
db="$1"
mysql --batch --skip-column-names --connect-timeout=5 "$db" <<'SQL'
SELECT CONCAT(
COALESCE(MIN(non_unique), -1), '|',
COALESCE(GROUP_CONCAT(column_name ORDER BY seq_in_index), ''), '|',
COALESCE(SUM(sub_part IS NOT NULL), 0)
)
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'aihr_practice_assignment'
AND index_name = 'idx_aihr_assignment_recent_content';
SQL
REMOTE
)" || fail "remote practice-assignment recent-content-index check failed: $remote_ssh:$remote_db"
[[ "$practice_assignment_recent_content_index" = "1|tenant_id,ext_party_id,source,content_hash,create_time|0" ]] \
|| fail "remote practice-assignment recent-content index invalid: expected full columns tenant_id,ext_party_id,source,content_hash,create_time; got ${practice_assignment_recent_content_index:-missing}"
local agent_schema_contract
agent_schema_contract="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$remote_ssh" bash -s -- "$remote_db" <<'REMOTE'
set -euo pipefail
@@ -597,6 +735,7 @@ REMOTE
|| fail "remote work-assistant schema missing required columns: $(printf '%s' "$missing_work_assistant_columns" | tr '\n' ', ' | sed 's/, $//')"
echo "remote_schema=64/64 $remote_ssh:$remote_db"
echo "remote_work_report_idempotency=3/3 $remote_ssh:$remote_db"
echo "remote_practice_curriculum_columns=13/13 $remote_ssh:$remote_db"
echo "remote_knowledge_category_columns=7/7 $remote_ssh:$remote_db"
echo "remote_broadcast_publish_audit_columns=3/3 $remote_ssh:$remote_db"
echo "remote_broadcast_publish_idempotency=3/3 $remote_ssh:$remote_db"
@@ -639,6 +778,10 @@ if [[ -n "${RELEASE_REMOTE_URL:-}" ]]; then
require_remote_schema
fi
if [[ "${RELEASE_VERIFY_REMOTE_SCHEMA:-false}" == "true" || "${RELEASE_VERIFY_REMOTE_BACKEND:-false}" == "true" ]]; then
require_remote_practice_schema_guard
fi
if [[ "${RELEASE_VERIFY_REMOTE_MATCH:-false}" == "true" ]]; then
remote_frontend_asset="$(curl -fsS --max-time 15 "$remote/" | sed -nE 's/.*src="(\/assets\/[^" ]+\.js)".*/\1/p' | head -n 1)"
remote_mobile_asset="$(curl -fsS --max-time 15 "$remote/h5/" | sed -nE 's/.*src="(\/h5\/assets\/[^" ]+\.js)".*/\1/p' | head -n 1)"
+5
View File
@@ -23,6 +23,9 @@ docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/aihr_personal_knowledge_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/aihr_model_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/aihr_practice_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260717_practice_five_position_catalog_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260723_practice_content_operations_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260724_practice_growth_curriculum_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/aihr_interview_result_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/aihr_org_snapshot_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260721_org_multi_project_membership_mysql8.sql"
@@ -35,6 +38,8 @@ docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260723_broadcast_targeting_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260722_broadcast_question_context_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260724_agent_orchestration_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260724_direct_feedback_mysql8.sql"
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue < "$ROOT_DIR/backend/script/sql/update/aihr_20260724_broadcast_attachment_mysql8.sql"
docker exec wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 ry-vue \
-e "select count(*) as tables_count from information_schema.tables where table_schema = 'ry-vue';"
+67 -1
View File
@@ -119,6 +119,72 @@ mysql "$DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260715_practice_sessio
practice_session_evidence_columns="$(mysql "$DB" -N -B -e "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='$DB' AND table_name='aihr_practice_session' AND column_name IN ('annotations_json','review_advice','incentive_point','satisfaction_score','satisfaction_comment');")"
test "$practice_session_evidence_columns" = '5'
# The growth curriculum migration must be able to upgrade the compact legacy
# practice tables used by older deployments, and be safe when applied again.
mysql "$DB" <<'SQL'
ALTER TABLE aihr_practice_session
ADD COLUMN tenant_id varchar(20) DEFAULT '000000',
ADD COLUMN mode varchar(20) DEFAULT 'mobile',
ADD COLUMN scenario_name varchar(100) DEFAULT NULL;
CREATE TABLE aihr_practice_scenario (
id bigint NOT NULL AUTO_INCREMENT,
tenant_id varchar(20) DEFAULT '000000',
scenario_code varchar(80) NOT NULL,
position varchar(80) DEFAULT '生活顾问',
difficulty tinyint DEFAULT 1,
goal varchar(1000) DEFAULT NULL,
enabled tinyint DEFAULT 1,
PRIMARY KEY (id),
UNIQUE KEY uk_aihr_practice_scenario (tenant_id, scenario_code)
) ENGINE=InnoDB;
INSERT INTO aihr_practice_scenario (tenant_id, scenario_code, position, difficulty, goal, enabled) VALUES
('000000', 'legacy-growth-enabled', '生活顾问', 2, 'legacy goal', 1),
('000000', 'legacy-growth-disabled', '保安', 3, 'legacy goal', 0);
CREATE TABLE aihr_practice_assignment (
id bigint NOT NULL AUTO_INCREMENT,
tenant_id varchar(20) DEFAULT '000000',
ext_party_id varchar(100) NOT NULL,
request_key varchar(100) DEFAULT NULL,
source varchar(30) NOT NULL DEFAULT 'manual',
content_hash char(64) DEFAULT NULL,
create_time datetime DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
SQL
practice_growth_migration="$ROOT_DIR/backend/script/sql/update/aihr_20260724_practice_growth_curriculum_mysql8.sql"
test -f "$practice_growth_migration"
mysql "$DB" < "$practice_growth_migration"
# A high-risk item may already have completed its first review when a release
# re-runs this idempotent migration. Its audit trail must survive for the
# second reviewer instead of being reset to an unreviewed item.
mysql "$DB" <<'SQL'
UPDATE aihr_practice_scenario
SET review_status = '待业务审核', risk_level = '高风险',
reviewer = '首审甲', reviewer_user_id = 101, reviewed_time = '2026-07-24 10:00:00'
WHERE scenario_code = 'legacy-growth-enabled';
SQL
mysql "$DB" < "$practice_growth_migration"
practice_growth_scenario_columns="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(column_name ORDER BY column_name) FROM information_schema.columns WHERE table_schema='$DB' AND table_name='aihr_practice_scenario' AND column_name IN ('growth_level','competency_code','collaboration_positions','review_status','risk_level','curriculum_version','reviewer','reviewer_user_id','reviewed_time','second_reviewer','second_reviewer_user_id','second_reviewed_time');")"
test "$practice_growth_scenario_columns" = 'collaboration_positions,competency_code,curriculum_version,growth_level,review_status,reviewed_time,reviewer,reviewer_user_id,risk_level,second_reviewed_time,second_reviewer,second_reviewer_user_id'
practice_growth_session_columns="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(column_name ORDER BY column_name) FROM information_schema.columns WHERE table_schema='$DB' AND table_name='aihr_practice_session' AND column_name IN ('position_snapshot','project_type_snapshot','growth_level','competency_code','collaboration_positions','curriculum_version','growth_confirmation_level','growth_confirmed_by','growth_confirmed_time');")"
test "$practice_growth_session_columns" = 'collaboration_positions,competency_code,curriculum_version,growth_confirmation_level,growth_confirmed_by,growth_confirmed_time,growth_level,position_snapshot,project_type_snapshot'
practice_growth_index="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index) FROM information_schema.statistics WHERE table_schema='$DB' AND table_name='aihr_practice_scenario' AND index_name='idx_aihr_practice_scenario_curriculum';")"
test "$practice_growth_index" = 'tenant_id,position,growth_level,competency_code,review_status,enabled'
practice_assignment_request_index="$(mysql "$DB" -N -B -e "SELECT CONCAT(MIN(non_unique),'|',GROUP_CONCAT(column_name ORDER BY seq_in_index),'|',SUM(sub_part IS NOT NULL)) FROM information_schema.statistics WHERE table_schema='$DB' AND table_name='aihr_practice_assignment' AND index_name='uk_aihr_assignment_request';")"
test "$practice_assignment_request_index" = '0|tenant_id,request_key,ext_party_id|0'
practice_assignment_recent_index="$(mysql "$DB" -N -B -e "SELECT CONCAT(MIN(non_unique),'|',GROUP_CONCAT(column_name ORDER BY seq_in_index),'|',SUM(sub_part IS NOT NULL)) FROM information_schema.statistics WHERE table_schema='$DB' AND table_name='aihr_practice_assignment' AND index_name='idx_aihr_assignment_recent_content';")"
test "$practice_assignment_recent_index" = '1|tenant_id,ext_party_id,source,content_hash,create_time|0'
mysql "$DB" -e "ALTER TABLE aihr_practice_assignment DROP INDEX uk_aihr_assignment_request, ADD INDEX uk_aihr_assignment_request (tenant_id, request_key(20), ext_party_id), DROP INDEX idx_aihr_assignment_recent_content, ADD KEY idx_aihr_assignment_recent_content (tenant_id, ext_party_id, source, content_hash);"
mysql "$DB" < "$practice_growth_migration"
practice_assignment_repaired_request_index="$(mysql "$DB" -N -B -e "SELECT CONCAT(MIN(non_unique),'|',GROUP_CONCAT(column_name ORDER BY seq_in_index),'|',SUM(sub_part IS NOT NULL)) FROM information_schema.statistics WHERE table_schema='$DB' AND table_name='aihr_practice_assignment' AND index_name='uk_aihr_assignment_request';")"
test "$practice_assignment_repaired_request_index" = '0|tenant_id,request_key,ext_party_id|0'
practice_assignment_repaired_recent_index="$(mysql "$DB" -N -B -e "SELECT CONCAT(MIN(non_unique),'|',GROUP_CONCAT(column_name ORDER BY seq_in_index),'|',SUM(sub_part IS NOT NULL)) FROM information_schema.statistics WHERE table_schema='$DB' AND table_name='aihr_practice_assignment' AND index_name='idx_aihr_assignment_recent_content';")"
test "$practice_assignment_repaired_recent_index" = '1|tenant_id,ext_party_id,source,content_hash,create_time|0'
practice_growth_statuses="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(CONCAT(scenario_code,':',review_status) ORDER BY scenario_code) FROM aihr_practice_scenario WHERE scenario_code LIKE 'legacy-growth-%';")"
test "$practice_growth_statuses" = 'legacy-growth-disabled:待业务审核,legacy-growth-enabled:待业务审核'
practice_growth_first_review="$(mysql "$DB" -N -B -e "SELECT CONCAT(COALESCE(reviewer,''),':',COALESCE(reviewer_user_id,'')) FROM aihr_practice_scenario WHERE scenario_code='legacy-growth-enabled';")"
test "$practice_growth_first_review" = '首审甲:101'
mysql "$DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql"
mysql "$DB" < "$ROOT_DIR/backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql"
evidence_tables="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(table_name ORDER BY table_name) FROM information_schema.tables WHERE table_schema='$DB' AND table_name IN ('aihr_practice_audio','aihr_practice_audio_upload','aihr_practice_calibration');")"
@@ -436,4 +502,4 @@ test "$agent_run_unique" = '0|run_id'
agent_user_index="$(mysql "$DB" -N -B -e "SELECT GROUP_CONCAT(column_name ORDER BY seq_in_index) FROM information_schema.statistics WHERE table_schema='$DB' AND table_name='aihr_agent_run' AND index_name='idx_aihr_agent_run_user';")"
test "$agent_user_index" = 'tenant_id,user_id,create_time'
echo "PASS: AIHR legacy schema migrations are idempotent, including Agent run audit, knowledge-space categories, practice evidence, knowledge feedback, candidate interview review, position/SOP qualification contracts, assistant capture, work-report idempotency, broadcast publish/withdraw audit with M1 targeting defaults and target-table contracts, v1 snapshots, and trusted broadcast question context"
echo "PASS: AIHR legacy schema migrations are idempotent, including Agent run audit, practice growth curriculum snapshots, knowledge-space categories, practice evidence, knowledge feedback, candidate interview review, position/SOP qualification contracts, assistant capture, work-report idempotency, direct feedback, broadcast publish/withdraw audit with M1 targeting and file attachment contracts, v1 snapshots, and trusted broadcast question context"