-- 外部 Excel 岗位全量快照同步。 -- 执行目标:HighGo/PostgreSQL 的 shz schema。 -- 本脚本为增量 migration;不删除现有 job、company、row_work 或监测数据。 BEGIN; -- 串行化本 migration 的并发执行,避免两个发布进程同时计算 MAX(id)+1。 -- 锁仅持续到本事务提交;重复执行不会改变已存在的业务记录或 Quartz 状态。 SELECT pg_advisory_xact_lock(hashtext('shz.migration_external_job_import')); ALTER TABLE "shz"."job" ADD COLUMN IF NOT EXISTS "external_import_flag" CHAR(1) NOT NULL DEFAULT '0', ADD COLUMN IF NOT EXISTS "external_source_code" VARCHAR(64), ADD COLUMN IF NOT EXISTS "external_source_name" VARCHAR(200), ADD COLUMN IF NOT EXISTS "job_fingerprint" VARCHAR(64), ADD COLUMN IF NOT EXISTS "job_content_fingerprint" VARCHAR(64), ADD COLUMN IF NOT EXISTS "external_batch_id" VARCHAR(64), ADD COLUMN IF NOT EXISTS "external_first_seen_at" TIMESTAMP, ADD COLUMN IF NOT EXISTS "external_last_seen_at" TIMESTAMP; COMMENT ON COLUMN "shz"."job"."external_import_flag" IS '是否外部 Excel 导入岗位:0否,1是'; COMMENT ON COLUMN "shz"."job"."job_fingerprint" IS '外部岗位身份指纹:FP_V1 + 来源编码 + 标准化岗位链接'; COMMENT ON COLUMN "shz"."job"."job_content_fingerprint" IS '外部岗位内容指纹:用于同一身份岗位的原地更新'; CREATE TABLE IF NOT EXISTS "shz"."job_import_batch" ( "batch_id" VARCHAR(64) PRIMARY KEY, "file_id" VARCHAR(64) NOT NULL, "file_name" VARCHAR(500) NOT NULL, "file_sha256" VARCHAR(64) NOT NULL, "snapshot_scope" TEXT, "fingerprint_version" VARCHAR(16) NOT NULL, "status" VARCHAR(20) NOT NULL, "total_rows" INTEGER NOT NULL DEFAULT 0, "valid_rows" INTEGER NOT NULL DEFAULT 0, "invalid_rows" INTEGER NOT NULL DEFAULT 0, "duplicate_rows" INTEGER NOT NULL DEFAULT 0, "insert_count" INTEGER NOT NULL DEFAULT 0, "update_count" INTEGER NOT NULL DEFAULT 0, "keep_count" INTEGER NOT NULL DEFAULT 0, "restore_count" INTEGER NOT NULL DEFAULT 0, "delete_count" INTEGER NOT NULL DEFAULT 0, "error_summary" TEXT, "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "completed_at" TIMESTAMP ); CREATE TABLE IF NOT EXISTS "shz"."job_import_row" ( "row_id" BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, "batch_id" VARCHAR(64) NOT NULL, "excel_row_number" INTEGER NOT NULL, "source_name" VARCHAR(200), "source_code" VARCHAR(64), "job_fingerprint" VARCHAR(64), "content_fingerprint" VARCHAR(64), "validation_status" VARCHAR(20) NOT NULL, "result_status" VARCHAR(20) NOT NULL, "error_message" TEXT, "raw_payload" TEXT, "job_id" BIGINT, "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS "shz"."job_import_source_alias" ( "alias_name" VARCHAR(200) PRIMARY KEY, "source_code" VARCHAR(64) NOT NULL, "website_id" BIGINT, "is_active" CHAR(1) NOT NULL DEFAULT '1', "snapshot_required" CHAR(1) NOT NULL DEFAULT '1', "remark" VARCHAR(500), "create_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "update_time" TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS "uk_job_external_active_fingerprint" ON "shz"."job" ("job_fingerprint") WHERE "external_import_flag" = '1' AND "del_flag" = '0'; CREATE INDEX IF NOT EXISTS "idx_job_import_batch_sha_status" ON "shz"."job_import_batch" ("file_sha256", "status"); CREATE INDEX IF NOT EXISTS "idx_job_import_row_batch" ON "shz"."job_import_row" ("batch_id", "excel_row_number"); CREATE INDEX IF NOT EXISTS "idx_job_external_source_active" ON "shz"."job" ("external_source_code", "external_import_flag", "del_flag"); -- Quartz 任务默认暂停,防止尚未配置 external-job-import URL/token 时发生无效拉取。 -- 完成环境配置后,在“定时任务”中启用即可;计划为每周三、周六 03:00。 INSERT INTO "shz"."sys_job" ( "job_id", "job_name", "job_group", "invoke_target", "cron_expression", "misfire_policy", "concurrent", "status", "create_by", "create_time", "update_by", "update_time", "remark" ) SELECT (SELECT COALESCE(MAX("job_id"), 0) + 1 FROM "shz"."sys_job"), '外部岗位全量同步', 'DEFAULT', 'jobCron.importLatestExternalSnapshot()', '0 0 3 ? * WED,SAT', '3', '1', '1', 'system', CURRENT_TIMESTAMP, '', NULL, '外部 Excel 全量快照同步;配置 external-job-import 后由管理员启用' WHERE NOT EXISTS ( SELECT 1 FROM "shz"."sys_job" WHERE "invoke_target" = 'jobCron.importLatestExternalSnapshot()' ); -- 为样例及当前采集范围初始化来源管理记录。名称相近的来源不自动合并。 INSERT INTO "shz"."website_management" ("website_name", "website_url", "is_active", "data_type", "del_flag", "create_by", "create_time", "remark") SELECT seed.website_name, '', '1', '2', '0', 'system', CURRENT_TIMESTAMP, '外部 Excel 岗位采集来源' FROM (VALUES ('石河子招聘网'), ('智联招聘'), ('天互招聘网'), ('猎聘网'), ('乌鲁木齐快聘网'), ('前程无忧') ) AS seed(website_name) WHERE NOT EXISTS ( SELECT 1 FROM "shz"."website_management" existing WHERE existing."website_name" = seed.website_name AND existing."del_flag" = '0' ); -- 以 alias_name 为幂等键;若此前仅完成了部分迁移,重跑时会补齐网站关联。 INSERT INTO "shz"."job_import_source_alias" AS target ("alias_name", "source_code", "website_id", "is_active", "snapshot_required", "remark") SELECT seed.alias_name, seed.source_code, website."website_id", '1', '1', '外部 Excel ORG 来源映射' FROM (VALUES ('石河子招聘网', 'shz_job_net'), ('智联招聘', 'zhaopin'), ('天互招聘网', 'tianhu_job'), ('猎聘网', 'liepin'), ('乌鲁木齐快聘网', 'wlmq_fast_job'), ('前程无忧', '51job') ) AS seed(alias_name, source_code) LEFT JOIN "shz"."website_management" website ON website."website_name" = seed.alias_name AND website."del_flag" = '0' ON CONFLICT ("alias_name") DO UPDATE SET "source_code" = EXCLUDED."source_code", "website_id" = EXCLUDED."website_id", "is_active" = EXCLUDED."is_active", "snapshot_required" = EXCLUDED."snapshot_required", "remark" = EXCLUDED."remark", "update_time" = CURRENT_TIMESTAMP WHERE target."source_code" IS DISTINCT FROM EXCLUDED."source_code" OR target."website_id" IS DISTINCT FROM EXCLUDED."website_id" OR target."is_active" IS DISTINCT FROM EXCLUDED."is_active" OR target."snapshot_required" IS DISTINCT FROM EXCLUDED."snapshot_required" OR target."remark" IS DISTINCT FROM EXCLUDED."remark"; -- 四字菜单:来源管理、入库监测、采集趋势、统计分析。 INSERT INTO "shz"."sys_menu" ( "menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "remark" ) SELECT (SELECT COALESCE(MAX("menu_id"), 0) + 1 FROM "shz"."sys_menu"), '招聘信息采集监测分析', 0, 99, 'recruit-monitor', '', '', 'RecruitMonitor', 1, 0, 'M', '0', '0', 'cms:recruit:root', 'chart', 'system', CURRENT_TIMESTAMP, '外部岗位采集监测分析根菜单' WHERE NOT EXISTS (SELECT 1 FROM "shz"."sys_menu" WHERE "perms" = 'cms:recruit:root'); INSERT INTO "shz"."sys_menu" ( "menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "remark" ) SELECT ids."base_id" + ROW_NUMBER() OVER (ORDER BY seed.order_num), seed.menu_name, parent."menu_id", seed.order_num, seed.path, seed.component, '', seed.route_name, 1, 0, 'C', '0', '0', seed.perms, 'table', 'system', CURRENT_TIMESTAMP, seed.remark FROM (VALUES ('来源管理', 1, 'recruit/source', 'Recruit/Source/index', 'RecruitSource', 'cms:recruit:source:list', '外部岗位来源与别名管理'), ('入库监测', 2, 'recruit/monitor', 'Recruit/Monitor/index', 'RecruitMonitor', 'cms:recruit:monitor:list', '外部岗位导入批次监测'), ('采集趋势', 3, 'recruit/trend', 'Recruit/Trend/index', 'RecruitTrend', 'cms:recruit:trend:list', '外部岗位采集趋势'), ('统计分析', 4, 'recruit/analysis', 'Recruit/Analysis/index', 'RecruitAnalysis', 'cms:recruit:analysis:list', '外部岗位统计分析') ) AS seed(menu_name, order_num, path, component, route_name, perms, remark) JOIN "shz"."sys_menu" parent ON parent."perms" = 'cms:recruit:root' CROSS JOIN (SELECT COALESCE(MAX("menu_id"), 0) AS "base_id" FROM "shz"."sys_menu") ids WHERE NOT EXISTS (SELECT 1 FROM "shz"."sys_menu" existing WHERE existing."perms" = seed.perms); INSERT INTO "shz"."sys_menu" ( "menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "remark" ) SELECT (SELECT COALESCE(MAX("menu_id"), 0) + 1 FROM "shz"."sys_menu"), '手动导入', parent."menu_id", 1, '', '', '', '', 1, 0, 'F', '0', '0', 'cms:recruit:import:manual', '#', 'system', CURRENT_TIMESTAMP, '手动拉取并导入最新外部 Excel' FROM "shz"."sys_menu" parent WHERE parent."perms" = 'cms:recruit:monitor:list' AND NOT EXISTS (SELECT 1 FROM "shz"."sys_menu" WHERE "perms" = 'cms:recruit:import:manual'); INSERT INTO "shz"."sys_menu" ( "menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "remark" ) SELECT (SELECT COALESCE(MAX("menu_id"), 0) + 1 FROM "shz"."sys_menu"), '来源维护', parent."menu_id", 1, '', '', '', '', 1, 0, 'F', '0', '0', 'cms:recruit:source:edit', '#', 'system', CURRENT_TIMESTAMP, '维护外部来源和来源别名' FROM "shz"."sys_menu" parent WHERE parent."perms" = 'cms:recruit:source:list' AND NOT EXISTS (SELECT 1 FROM "shz"."sys_menu" WHERE "perms" = 'cms:recruit:source:edit'); INSERT INTO "shz"."sys_menu" ( "menu_id", "menu_name", "parent_id", "order_num", "path", "component", "query", "route_name", "is_frame", "is_cache", "menu_type", "visible", "status", "perms", "icon", "create_by", "create_time", "remark" ) SELECT (SELECT COALESCE(MAX("menu_id"), 0) + 1 FROM "shz"."sys_menu"), '监测维护', parent."menu_id", 2, '', '', '', '', 1, 0, 'F', '0', '0', 'cms:recruit:monitor:edit', '#', 'system', CURRENT_TIMESTAMP, '维护导入监测记录' FROM "shz"."sys_menu" parent WHERE parent."perms" = 'cms:recruit:monitor:list' AND NOT EXISTS (SELECT 1 FROM "shz"."sys_menu" WHERE "perms" = 'cms:recruit:monitor:edit'); INSERT INTO "shz"."sys_role_menu" ("role_id", "menu_id") SELECT role."role_id", menu."menu_id" FROM "shz"."sys_role" role JOIN "shz"."sys_menu" menu ON menu."perms" IN ( 'cms:recruit:root', 'cms:recruit:source:list', 'cms:recruit:monitor:list', 'cms:recruit:trend:list', 'cms:recruit:analysis:list', 'cms:recruit:import:manual', 'cms:recruit:source:edit', 'cms:recruit:monitor:edit' ) WHERE role."role_key" = 'admin' AND NOT EXISTS ( SELECT 1 FROM "shz"."sys_role_menu" existing WHERE existing."role_id" = role."role_id" AND existing."menu_id" = menu."menu_id" ); COMMIT;