# AIHR BRD 生产迁移 Runbook 本文件只覆盖 AIHR 阶段一 BRD 已落地代码所需的 MySQL 8 结构和内置内容迁移。它不执行正式组织同步、试点训练、人工校准、SOP 评审、案例导入或满意度补录;这些属于业务数据和正式验收,必须单独审批。 ## 当前生产基线 2026-07-18 已在生产发布功能包提交 `064816a7`: - 22 个版本化迁移按本文件顺序执行完成,目标结构核验为 `35/35`; - 最后一条排序规则迁移把 19 张发布表对齐到 `aihr_knowledge_info.tenant_id` 的实际排序规则,核验为 `19/19`; - 管理端、H5、后端产物与本地发布产物哈希一致,`wygj-aihr.service` 为 `active`; - 根站、H5、租户接口、员工首页 API 和关键管理页通过生产回归; - 当前发布备份位于 `/opt/wygj/backups/ask-memory-20260718-2130`,内含完整数据库、JAR、H5 和 SHA-256 清单。 上述事实只证明本批版本和 schema 已发布。正式 OTP、真机能力、角色权限正反例和严格试点窗口仍需按发布后回归及 `DEMO_ACCEPTANCE.md` 验收。 2026-07-20 增量把“项目服务记忆最小切片”扩展为工作助手确认式统一采集:显式“记一下”生成候选,用户确认后按 `PRIVATE/COMPANY` 写入 `aihr_assistant_capture`;个人记录仅本人可见,公司记录先保持 `PENDING`,正式接收端和回执尚未实现。旧 `aihr_service_memory/version` 继续兼容项目记录;同批会话式“工作上报”继续使用既有 `aihr_work_report`,并补 `request_key/request_hash` 和唯一索引。 2026-07-21 已在生产发布项目化工作助手与今日工作成果提交 `4536d607`,并发布确认卡首屏修正 `11250811`: - 执行 `aihr_20260721_org_multi_project_membership_mysql8.sql` 与 `aihr_20260721_work_assistant_results_mysql8.sql`,目标新表、字段和组织多项目唯一约束核验通过; - 后端 JAR、H5 入口资源与本地发布产物哈希一致,`wygj-aihr.service` 为 `active`; - 正式员工账号完成项目读取、伪造项目拒绝、文字巡检确认卡、候选撤销和按员工 + 项目 + 日期幂等生成成果回归; - 当前正式员工账号只返回一个授权项目“银河湾星苑”,故生产仅证明单项目自动选择,多项目名称下拉继续以本地双项目授权数据为证; - 本次完整备份位于 `/opt/wygj/backups/work-assistant-20260721-044923`,包含数据库、旧 JAR、完整 H5 和 SHA-256 清单。 上述事实仍不代表生产真机语音/照片/视频、主管正式账号、正式组织多项目 dry-run 或外部线索/工单/考勤回执已验收。个人文件、网页收藏、独立个人向量库、自动派单和未经确认的自动外发仍不在本轮范围。 2026-07-23 已完成“大喇叭 + 问模块纠偏”相关的一轮全量迁移与重新发布,发布源为本地 `f023417a`(含业务提交 `4b31848e`): - 本批按当时清单执行 `aihr_20260722_knowledge_category_mysql8.sql`、`aihr_20260722_broadcast_mysql8.sql`、`aihr_20260722_broadcast_question_context_mysql8.sql` 与 `aihr_personal_knowledge_mysql8.sql`;后者建立 10 张 `aihr_personal_*` 表,并配置私有 `personal-minio` 存储与 `ruoyi-personal` bucket; - 当时 `release-preflight` 的远端只读核验为 schema `57/57`,且 H5、管理端资源、后端 AIHR 模块均与本地构建一致,`wygj-aihr.service` 为 `active`。后续只读复核已确认 M1 的 `aihr_broadcast_target_rule`、`aihr_broadcast_target_recipient` 及必读字段存在;这只证明 schema,不能替代 M1 认证态定向/必读闭环验收; - 本次完整备份位于 `/opt/wygj/backups/aihr-full-20260723-094600-f023417a`,包含数据库、旧静态资源和旧 JAR; - 发布后仅完成公开接口和未登录 H5 页面的基础检查。正式认证账号的“管理发布—员工阅读—消息追问”闭环、真机媒体及完整个人资料能力仍未作为生产验收完成。 ## 发布前置条件 1. 确认发布窗口、DB 备份和回滚负责人;迁移脚本包含 `ALTER TABLE`,不能在无备份状态执行。 2. 发布前先执行 `scripts/release-preflight.sh` 的本地只读检查。完整包发布后,必须按 `docs/DEV_SETUP.md` 使用 `RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true ./scripts/release-preflight.sh` 核对线上静态资源、AIHR 模块和必需表;schema 检查只读、不执行迁移。只校验 H5 资源不能替代完整包复核。 3. 确认迁移目标库、租户和字符集为 MySQL 8 / `utf8mb4`;脚本只允许在目标业务库执行。 4. 生产组织同步凭据、AI 供应商留存/费用口径和正式试点窗口仍未闭合时,不要把迁移完成宣称为 BRD 验收完成。 ## 执行顺序 在生产数据库连接上按以下顺序执行,每个脚本执行后记录退出码和关键表结构。所有脚本均设计为可重复执行,但仍应按顺序执行。 ```bash mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_practice_catalog_schema_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_practice_brd_scenarios_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_brd_sop_seed_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_prompt_template_seed_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260715_candidate_interview_review_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_practice_evidence_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_case_media_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_case_learning_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_practice_five_dimensions_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260715_practice_session_evidence_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260723_practice_m2_help_event_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_daily_drill_score_mode_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_sop_requester_identity_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260715_knowledge_feedback_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260715_tenant_collation_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260716_knowledge_space_platform_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260714_org_hire_date_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260715_position_sop_qualification_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260717_practice_assignment_batch_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260717_practice_assignment_contract_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260717_learning_closure_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260717_practice_five_position_catalog_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260717_web_ai_question_reward_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260718_knowledge_conversation_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260718_release_collation_compat_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260718_unify_all_collations_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260719_service_memory_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260721_org_multi_project_membership_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260721_work_assistant_results_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260719_work_report_idempotency_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260719_web_ai_database_secret_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260719_web_search_qwen_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260723_tenant_org_governance_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260722_knowledge_category_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260722_broadcast_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260723_broadcast_targeting_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/update/aihr_20260722_broadcast_question_context_mysql8.sql mysql --default-character-set=utf8mb4 "$DB_NAME" < backend/script/sql/aihr_personal_knowledge_mysql8.sql ``` 顺序原因:场景补充依赖三张场景/Rubric 基础表;五维迁移依赖场景和 Rubric;岗位/SOP/任务/资格迁移只补正式数据契约,不写业务行,也不代表岗位适用范围、任务规则或资格标准已经获得 HR 确认。住宅 SOP 和 Prompt 迁移只补内置内容,不代表内容已完成业务复核。知识会话迁移只新增短期上下文表,不生成业务对话。排序规则兼容迁移先对齐本批发布表,全量排序规则迁移再统一历史 `aihr_*` 表;两者都读取目标库 `aihr_knowledge_info.tenant_id` 的实际排序规则。服务记忆迁移创建候选、统一采集、兼容项目记录和版本留痕四张表;工作上报幂等迁移随后只补原表列与唯一索引;知识分类迁移新增空间内分类表及附件成员的可空 `category_id`,不迁移、删除或扩大任何空间授权与检索范围;银城大喇叭 M0 迁移创建消息、阅读和不可变 v1 快照三表,为已存在消息补一次快照,并补发布请求键/载荷哈希、撤回原因及 `(tenant_id,published_by,publish_request_key)` 唯一约束。紧接着 M1 定向迁移补充必读与目标载荷字段,以及规则快照、命中对象快照两表;它只决定提醒/必读对象,不改变全租户公开频道的可见性。随后,消息追问迁移只给短期会话增加可空 `broadcast_message_id`;它不保存消息正文,并用于约束同一会话不能切换公司消息。历史普通会话保持 `NULL`。个人资料迁移最后创建独立 `aihr_personal_*` 表,并从当前租户既有 MinIO 配置派生私有 `personal-minio` 配置;`ruoyi-personal` bucket 必须由受控运维流程创建和验证,不能在文档或日志中输出访问密钥。它们同样对齐目标排序规则,兼容历史生产库与全新 MySQL 8 数据库。 ## 迁移后只读核验 执行后只读检查结构和内置内容,不修改业务数据: ```sql SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name IN ( 'aihr_prompt_template', 'aihr_practice_scenario', 'aihr_practice_rubric', 'aihr_practice_rubric_dimension', 'aihr_learning_question', 'aihr_practice_audio', 'aihr_practice_audio_upload', 'aihr_practice_calibration', 'aihr_practice_help_event', 'aihr_knowledge_gap', 'aihr_knowledge_answer_feedback', 'aihr_knowledge_space_grant', 'aihr_knowledge_app', 'aihr_knowledge_app_space', 'aihr_knowledge_category', 'aihr_knowledge_query_log', 'aihr_knowledge_admin_audit', 'aihr_knowledge_conversation', 'aihr_practice_question_feedback', 'aihr_onboard_exam', 'aihr_onboard_exam_target', 'aihr_onboard_exam_question', 'aihr_onboard_exam_attempt', 'aihr_onboard_exam_answer', 'aihr_web_search_provider', 'aihr_web_ai_secret', 'aihr_web_ai_query_audit', 'aihr_community_question', 'aihr_community_answer', 'aihr_incentive_rule', 'aihr_points_ledger', 'aihr_candidate_material', 'aihr_interview_result', 'aihr_candidate_employee_link', 'aihr_position_responsibility', 'aihr_sop_applicability', 'aihr_onboard_task', 'aihr_qualification_gate', 'aihr_org_directory', 'aihr_tenant_org_binding', 'aihr_memory_candidate', 'aihr_assistant_capture', 'aihr_assistant_capture_status_log', 'aihr_daily_work_result', 'aihr_service_memory', 'aihr_service_memory_version', 'aihr_work_report', 'aihr_broadcast_message', 'aihr_broadcast_read', 'aihr_broadcast_version', 'aihr_broadcast_target_rule', 'aihr_broadcast_target_recipient', 'aihr_personal_space', 'aihr_personal_item', 'aihr_personal_fragment', 'aihr_personal_chat_session', 'aihr_personal_chat_message', 'aihr_personal_cleanup_job', 'aihr_personal_ocr_job', 'aihr_personal_ocr_page', 'aihr_personal_export_task', 'aihr_personal_publish_request' ) ORDER BY table_name; SELECT table_name, column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND ( (table_name = 'aihr_case_record' AND column_name IN ('media_oss_id', 'media_url', 'learning_points', 'supervisor_comment')) OR (table_name = 'aihr_practice_session' AND column_name IN ('dim_task_completion', 'dim_response_timeliness', 'response_latency_ms', 'annotations_json', 'review_advice', 'incentive_point', 'satisfaction_score', 'satisfaction_comment')) OR (table_name = 'aihr_practice_scenario' AND column_name IN ('content_version', 'content_hash')) OR (table_name = 'aihr_practice_assignment' AND column_name IN ('score', 'score_mode', 'due_date', 'request_key', 'content_version', 'question_snapshot', 'reference_answer_snapshot', 'content_hash')) OR (table_name = 'aihr_sop_answer_review' AND column_name IN ('prompt_version', 'requester_ext_party_id')) OR (table_name = 'aihr_knowledge_info' AND column_name IN ('code', 'space_type', 'sensitivity_level', 'status')) OR (table_name = 'aihr_knowledge_attach' AND column_name = 'category_id') OR (table_name = 'aihr_knowledge_upload_item' AND column_name = 'space_codes_json') OR (table_name = 'aihr_org_snapshot' AND column_name = 'hire_date') OR (table_name = 'aihr_candidate_material' AND column_name IN ('reviewer', 'reviewed_time')) OR (table_name = 'aihr_interview_result' AND column_name IN ('questions_json', 'reviewed_score', 'review_status', 'reviewer', 'review_note', 'reviewed_time')) OR (table_name = 'aihr_position_responsibility' AND column_name IN ('position_code', 'responsibility_text', 'sop_id', 'sop_code')) OR (table_name = 'aihr_sop_applicability' AND column_name IN ('sop_id', 'sop_code', 'project_type', 'industry', 'org_scope')) OR (table_name = 'aihr_onboard_task' AND column_name IN ('ext_party_id', 'position_code', 'task_type', 'status', 'assign_by')) OR (table_name = 'aihr_qualification_gate' AND column_name IN ('ext_party_id', 'position_code', 'cert_id', 'passed', 'valid_thru')) OR (table_name = 'aihr_work_report' AND column_name IN ('request_key', 'request_hash')) OR (table_name = 'aihr_broadcast_message' AND column_name IN ('publish_request_key', 'publish_request_hash', 'withdraw_reason')) OR (table_name = 'aihr_memory_candidate' AND column_name IN ('work_date', 'source_snapshot_json')) OR (table_name = 'aihr_assistant_capture' AND column_name IN ('work_date', 'source_snapshot_json', 'business_status')) OR (table_name = 'aihr_knowledge_conversation' AND column_name IN ('project_code', 'broadcast_message_id')) OR (table_name = 'aihr_daily_work_result' AND column_name = 'content_hash') OR (table_name = 'aihr_web_search_provider' AND column_name IN ('agent_id', 'agent_version')) ) ORDER BY table_name, column_name; SELECT MIN(non_unique) AS non_unique, GROUP_CONCAT(column_name ORDER BY seq_in_index) AS indexed_columns, SUM(sub_part IS NOT NULL) AS prefix_columns FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'aihr_broadcast_message' AND index_name = 'uk_aihr_broadcast_message_publish_request'; SELECT bucket_name, access_policy, CASE WHEN COALESCE(access_key, '') <> '' AND COALESCE(secret_key, '') <> '' THEN 'configured' ELSE 'missing' END AS credential_state FROM sys_oss_config WHERE tenant_id = '000000' AND config_key = 'personal-minio'; SELECT COUNT(*) AS built_in_prompt_templates FROM aihr_prompt_template WHERE tenant_id = '000000' AND code IN ('summary_card', 'sop_answer', 'mentor_rewrite', 'review_annotation', 'prep_card', 'supervision_image_analysis_maintenance'); SELECT position, COUNT(*) AS scenario_count, SUM(enabled = 0) AS disabled_pending_review FROM aihr_practice_scenario WHERE tenant_id = '000000' GROUP BY position ORDER BY position; SELECT question_type, COUNT(*) AS question_count, SUM(enabled = 0) AS disabled_pending_review FROM aihr_learning_question WHERE tenant_id = '000000' GROUP BY question_type ORDER BY question_type; ``` 预期是三张场景/Rubric 表、岗位题库、训练反馈表与训练求助事件表、五张考试表、三张全网检索表、问题榜/激励六张表、三张证据表、知识平台治理表和空间内分类表、三张候选人/面试表、四张岗位/SOP/任务/资格契约表、平台组织目录与租户绑定表、四张记忆表、采集状态历史表、每日成果表、原工作上报表、三张大喇叭消息/阅读/v1快照表、两张 M1 定向规则/对象快照表及十张独立个人资料表均存在。`57/57` 只代表当时较窄的发布清单;后续线上只读复核已确认 M1 的目标表和必读字段,但当前线上不存在 `aihr_practice_help_event`,因此不得把包含该表的 M2 完整清单写成已发布。工作上报幂等字段为 `2/2`,大喇叭发布/撤回审计字段为 `3/3`,其发布幂等索引为 `0 | tenant_id,published_by,publish_request_key | 0`,M1 必读/目标载荷字段契约为 `2/2`,两张目标表字段契约为 `16/16`,定向规则与对象快照唯一约束分别为 `4/4`、`3/3`,公司消息追问会话字段为 `1/1`,工作助手新增字段为 `8/8`,会话证据字段为 `8/8`,专项批量/内容快照字段为 `8/8`,候选资料审核字段为 `2/2`,面试复核字段为 `6/6`,字段查询覆盖所有列,内置 Prompt 数量为 `6`。私有资料配置只应返回 `ruoyi-personal`、私有策略和 `configured` 状态,不显示访问密钥。五个岗位各有 `20` 个场景,`daily/special` 各有 `100` 道题;其中 `14` 个未完成正式审核的高风险场景及其 `28` 道题保持禁用。岗位/SOP/任务/资格表及新记忆表为空是允许的;这只证明 schema/seed 迁移完成,不证明已有员工确认过记忆、完整个人知识库或严格试点完成。 ## 迁移后应用回归 1. 重启 `wygj-aihr.service`,确认状态为 `active`。 2. 检查根站、`/h5/`、租户接口和员工首页 API 的 HTTP 与业务码均为 `200`。 3. 用正式测试账号完成员工端训练、主管复盘和管理端场景/Prompt 只读检查;不要使用开发固定验证码验证生产安全策略。 4. 用正式测试账号在“工作助手”选择当前项目并说“帮我记一下三栋 3203 需要保洁服务”;确认前 `aihr_assistant_capture` 不得新增。分别验证 `PRIVATE` 仅本人可召回、`COMPANY` 只显示 `PENDING` 且没有虚假送达回执;新开对话按房号查询必须只命中对应项目记录,双击确认不得重复写入。测试表达如不需长期保留,应在确认卡选择“暂不保存”,不要直接删除生产审计记录。 5. 在“今日工作成果”按员工 + 项目 + 日期重复生成两次,内容不变时版本哈希必须一致;员工访问团队结果、主管访问非授权项目必须被拒绝。生产无主管正式测试账号时必须明确记录未验收,不以本地 seed 代替。 6. 用员工测试账号进入“成果投稿”,确认其仍只承载优秀案例、操作视频、完整 SOP 和有用知识,不回退为日常事实表单。重复提交同一 `request_key` 只有一条 `PENDING` 记录;普通员工审核仍为 `403`,审核通过不自动写入案例、知识或积分表。 7. 用 `superadmin` 或 `hr_operator` 在当前租户发布同一 `requestId` 两次:同内容必须只生成一条消息和一个 v1 快照,改动正文后必须返回 `409`。撤回时提交非空原因,员工端随即不可见;重复撤回不得改写首个撤回人、时间或原因。再确认管理列表不跨租户返回消息。 8. 完整包发布后运行 `RELEASE_VERIFY_REMOTE_MATCH=true RELEASE_VERIFY_REMOTE_BACKEND=true RELEASE_VERIFY_REMOTE_SCHEMA=true ./scripts/release-preflight.sh`;必须同时获得线上资源、AIHR 模块和 schema 成功结果。该检查不替代第 7 项的认证态业务闭环。 9. 迁移完成后再按同一正式时间窗运行: ```bash AIHR_PILOT_TENANT_ID=000000 \ AIHR_PILOT_START_DATE=YYYY-MM-DD \ AIHR_PILOT_END_DATE=YYYY-MM-DD \ AIHR_PILOT_STRICT=true ./scripts/demo-check.sh ``` 严格门禁中的校准、SOP 可用率、完训率、满意度、正式组织身份和案例数量仍必须来自真实业务数据;不允许用本 runbook 的内置 seed 或开发库样本替代。