-- Import OMS existing project-space opportunities into CRM. -- -- Source workbook: /Users/kangwenjing/Downloads/OMS存量项目空间(研发转录CRM)0423.xlsx -- Source sheet: OMS已立项 -- -- Re-runnable behavior: -- - crm_customer is reused by generated customer_code, or by customer_name + default owner/null owner. -- - crm_opportunity is upserted by opportunity_code, which comes from OMS 项目ID. -- - The Excel 售前ID/售前 columns are imported as pre_sales_id/pre_sales_name. -- - owner_user_id is still required by crm_opportunity, so this script uses a default enabled user. -- -- Mapping notes: -- - 项目ID -> crm_opportunity.opportunity_code -- - 项目名称 -> crm_opportunity.opportunity_name -- - 最终客户名称/区域/行业 -> crm_customer -- - 区域 -> crm_customer.province and crm_opportunity.project_location, normalized with 省/市/自治区 suffix -- - 售前ID/售前 -> crm_opportunity.pre_sales_id/pre_sales_name -- - 下单时间 -> crm_opportunity.expected_close_date -- - 项目状态 = 已下单 -> stage/status won, archived true -- - 项目状态 = 重点跟进 -> stage S4A when dictionary exists, otherwise business_negotiation -- - 项目状态 = 商机 -> stage S0 when dictionary exists, otherwise initial_contact -- -- If your SQL client reports "current transaction is aborted", run this first: -- rollback; -- -- This script intentionally avoids an explicit begin/commit so GUI SQL clients can -- execute it statement-by-statement without getting stuck in an aborted transaction. rollback; drop table if exists tmp_oms_opportunity_import; drop table if exists tmp_oms_area_map; drop table if exists tmp_oms_default_owner; drop table if exists tmp_oms_dict_values; drop table if exists tmp_oms_stage_codes; create temp table tmp_oms_opportunity_import ( row_no integer not null, project_status varchar(50) not null, project_id varchar(50) not null, project_name varchar(200) not null, confidence_pct varchar(1) not null, customer_name varchar(200) not null, province varchar(50), industry varchar(50), pre_sales_id bigint, pre_sales_name varchar(100) not null, amount numeric(18, 2) not null, order_date date ) on commit preserve rows; insert into tmp_oms_opportunity_import ( row_no, project_status, project_id, project_name, confidence_pct, customer_name, province, industry, pre_sales_id, pre_sales_name, amount, order_date ) values (2, '重点跟进', 'V001385', '贵州省公安厅云桌面增补项目', 'B', '贵州省公安厅', '贵州', '政府', 104, '陈畅', 71152, '2026-04-30'), (3, '重点跟进', 'V001427', '低压电工上岗证鉴定中心设备采购项目', 'B', '东莞职业技术学院', '广东', '教育科研', 105, '杨钊', 80000, '2026-04-30'), (4, '重点跟进', 'V001496', '江门市中心医院云终端设备的采购项目', 'B', '江门市中心医院', '广东', '医疗', 105, '杨钊', 500000, '2026-04-08'), (5, '重点跟进', 'V001464', '兰州石化职业技术大学云桌面升级改造项目', 'B', '兰州石化职业技术大学', '甘肃', '教育科研', 128, '骆栋', 1600000, '2026-05-01'), (6, '重点跟进', 'V001527', '池州职业技术学院云桌面', 'B', '池州职业技术学院', '安徽', '教育科研', 109, '韦东东', 2200000, null), (7, '重点跟进', 'V001377', '马鞍山GA云桌面扩容', 'B', '马鞍山公安局', '安徽', '政府', 109, '韦东东', 100000, null), (8, '重点跟进', 'V001454', '宁阳县职业中等专业学校云桌面续保(2026)', 'A', '宁阳县职业中等专业学校', '山东', '教育科研', 110, '魏光耀', 23850, '2026-05-01'), (9, '重点跟进', 'V001434', '河北省电视台云桌面定制开发', 'C', '河北省电视台', '河北', '政府', 110, '魏光耀', 67200, '2026-04-01'), (10, '重点跟进', 'V001474', '邯郸市邯山区阳光实验中学云桌面项目(2026)', 'A', '邯郸市邯山区阳光实验中学', '河北', '教育科研', 110, '魏光耀', 133000, '2026-04-01'), (11, '重点跟进', 'V001446', '山东旅游职业学院云教室项目', 'C', '山东旅游职业学院', '山东', '教育科研', 110, '魏光耀', 529671, '2026-05-01'), (12, '商机', 'V001253', '中国生物云桌面项目', 'B', '中国生物技术股份有限公司', '北京', '企业', 110, '魏光耀', 250000, '2026-10-01'), (13, '商机', 'V001441', '中铁第五勘察设计院云桌面项目', 'B', '中铁第五勘察设计院集团有限公司', '北京', '企业', 110, '魏光耀', 92915, '2026-06-01'), (14, '商机', 'V001365', '泰山玻璃纤维(太原)云桌面项目', 'B', '泰山玻璃纤维有限公司', '北京', '企业', 110, '魏光耀', 90276.5, '2026-04-03'), (15, '商机', 'V001437', '北京生物国产化云桌面项目', 'C', '北京生物制品研究所有限责任公司', '北京', '企业', 110, '魏光耀', 91100, '2026-10-01'), (16, '商机', 'V001445', '清华大学生命科学学院云桌面项目', 'B', '清华大学', '北京', '教育科研', 110, '魏光耀', 89032, '2026-06-01'), (17, '重点跟进', 'V001521', '北京北重汽轮电机C3300终端扩容', 'A', '北京北重汽轮电机有限责任公司', '北京', '企业', 110, '魏光耀', 33600, '2026-04-30'), (18, '重点跟进', 'V001507', '青岛物元半导体云桌面扩容(2026)', 'A', '物元半导体技术(青岛)有限公司', '山东', '企业', 110, '魏光耀', 217107, '2026-06-30'), (19, '重点跟进', 'V001513', '东方物探云桌面扩容项目', 'A', '中国石油集团东方地球物理勘探有限责任公司', '北京', '电力能源', 110, '魏光耀', 69260.4, '2026-04-30'), (20, '重点跟进', 'V001494', '天津国土资源和房屋职业学院云教室项目(终端增补)2026', 'A', '天津国土资源和房屋职业学院', '天津', '教育科研', 110, '魏光耀', 899000, '2026-06-30'), (21, '重点跟进', 'V001530', '山东博源精密机械云桌面', 'B', '山东博源精密机械有限公司', '山东', '企业', 110, '魏光耀', 758400, '2026-06-30'), (22, '重点跟进', 'V001153', '中建集团云桌面项目', 'A', '中国建筑集团有限公司', '北京', '企业', 110, '魏光耀', 1292280, '2026-06-30'), (23, '重点跟进', 'V001166', '教育考试院云桌面项目', 'A', '教育部教育考试院', '北京', '教育科研', 110, '魏光耀', 84965, '2026-06-30'), (24, '商机', 'V001313', '吉林移动云桌面扩容', 'A', '吉林移动', '吉林', '企业', 106, '杨坤融', 890000, '2026-06-12'), (25, '重点跟进', 'V001399', '佳木斯大学云桌面项目', 'B', '佳木斯大学', '内蒙古', '教育科研', 105, '杨钊', 895210, '2026-05-15'), (26, '商机', 'V001523', '雅江集团云桌面项目', 'C', '中国雅江集团有限公司', '四川', '企业', 105, '杨钊', 5000000, '2026-04-30'), (27, '商机', 'V001515', '福建闽侯公安', 'B', '闽侯公安局', '福建', '政府', 130, '张锦杰', 150000, '2026-04-30'), (28, '商机', 'V001435', '东亚银行Citrix桌面云国产化项目', 'B', '上海东亚银行', '上海', '金融', 130, '张锦杰', 300000, '2026-06-01'), (29, '商机', 'V001522', '上海泽丰半导体云桌面项目', 'C', '上海泽丰半导体', '上海', '企业', 130, '张锦杰', 800000, '2026-06-01'), (30, '商机', 'V001416', '海南移动自研1500点桌面云', 'C', '海南省移动', '海南', '企业', 130, '张锦杰', 4500000, '2026-06-30'), (31, '重点跟进', 'V000001', '江西省余江区总医院人民医院项目', 'A', '江西省余江区总医院人民医院', '江西', '医疗', 130, '张锦杰', 11000, '2026-04-02'), (32, '重点跟进', 'V001281', '厦门翔安机场口岸通关港口', 'C', '厦门翔安机场', '福建', '企业', 130, '张锦杰', 310000, null), (33, '重点跟进', 'V001299', '抚州公安300点VDI利旧', 'A', '抚州市公安局', '江西', '政府', 130, '张锦杰', 300000, '2026-04-30'), (34, '重点跟进', 'V001493', '江西新建区人民医院-二期扩容', 'B', '江西新建区人民医院', '江西', '医疗', 130, '张锦杰', 422830, '2026-05-15'); create temp table tmp_oms_default_owner ( owner_user_id bigint not null, owner_source text not null ) on commit preserve rows; create temp table tmp_oms_area_map ( raw_area varchar(50) not null primary key, normalized_area varchar(50) not null ) on commit preserve rows; create temp table tmp_oms_stage_codes ( initial_stage varchar(50) not null, follow_stage varchar(50) not null ) on commit preserve rows; create temp table tmp_oms_dict_values ( opportunity_type varchar(50) not null ) on commit preserve rows; insert into tmp_oms_stage_codes (initial_stage, follow_stage) values ('initial_contact', 'business_negotiation'); insert into tmp_oms_dict_values (opportunity_type) values ('新建'); insert into tmp_oms_area_map (raw_area, normalized_area) select distinct province as raw_area, case province when '北京' then '北京市' when '上海' then '上海市' when '天津' then '天津市' when '重庆' then '重庆市' when '内蒙古' then '内蒙古自治区' when '广西' then '广西壮族自治区' when '西藏' then '西藏自治区' when '宁夏' then '宁夏回族自治区' when '新疆' then '新疆维吾尔自治区' when '香港' then '香港特别行政区' when '澳门' then '澳门特别行政区' when '河北' then '河北省' when '山西' then '山西省' when '辽宁' then '辽宁省' when '吉林' then '吉林省' when '黑龙江' then '黑龙江省' when '江苏' then '江苏省' when '浙江' then '浙江省' when '安徽' then '安徽省' when '福建' then '福建省' when '江西' then '江西省' when '山东' then '山东省' when '河南' then '河南省' when '湖北' then '湖北省' when '湖南' then '湖南省' when '广东' then '广东省' when '海南' then '海南省' when '四川' then '四川省' when '贵州' then '贵州省' when '云南' then '云南省' when '陕西' then '陕西省' when '甘肃' then '甘肃省' when '青海' then '青海省' when '台湾' then '台湾省' else province end as normalized_area from tmp_oms_opportunity_import where province is not null and btrim(province) <> ''; do $$ declare uid_column text; username_filter text := ''; status_filter text := ''; deleted_filter text := ''; default_owner_sql text; invalid_confidence text; duplicate_project_ids text; begin select string_agg(format('第%s行:%s', row_no, confidence_pct), ', ' order by row_no) into invalid_confidence from tmp_oms_opportunity_import where confidence_pct not in ('A', 'B', 'C'); if invalid_confidence is not null then raise exception 'OMS存量商机导入失败:把握度仅支持 A/B/C,异常数据:%', invalid_confidence; end if; select string_agg(project_id, ', ' order by project_id) into duplicate_project_ids from ( select project_id from tmp_oms_opportunity_import group by project_id having count(*) > 1 ) duplicated; if duplicate_project_ids is not null then raise exception 'OMS存量商机导入失败:项目ID重复,无法按 opportunity_code 幂等导入:%', duplicate_project_ids; end if; if to_regclass('public.sys_user') is null then raise exception 'OMS存量商机导入失败:sys_user 表不存在,无法确定商机默认负责人 owner_user_id'; end if; select case when exists ( select 1 from information_schema.columns where table_schema = 'public' and table_name = 'sys_user' and column_name = 'user_id' ) then 'user_id' when exists ( select 1 from information_schema.columns where table_schema = 'public' and table_name = 'sys_user' and column_name = 'id' ) then 'id' else null end into uid_column; if uid_column is null then raise exception 'OMS存量商机导入失败:sys_user 缺少 user_id/id 字段,无法写入 owner_user_id'; end if; if exists ( select 1 from information_schema.columns where table_schema = 'public' and table_name = 'sys_user' and column_name = 'status' ) then status_filter := ' and coalesce(u.status, 1) = 1'; end if; if exists ( select 1 from information_schema.columns where table_schema = 'public' and table_name = 'sys_user' and column_name = 'is_deleted' ) then deleted_filter := ' and coalesce(u.is_deleted, 0) = 0'; end if; if exists ( select 1 from information_schema.columns where table_schema = 'public' and table_name = 'sys_user' and column_name = 'username' ) then username_filter := 'case when btrim(coalesce(u.username::text, '''')) = ''admin'' then 0 else 1 end,'; end if; default_owner_sql := format( 'insert into tmp_oms_default_owner (owner_user_id, owner_source) select u.%1$I::bigint, ''default sys_user for OMS import'' from public.sys_user u where u.%1$I is not null%2$s%3$s order by %4$s u.%1$I asc limit 1', uid_column, status_filter, deleted_filter, username_filter ); execute default_owner_sql; if not exists (select 1 from tmp_oms_default_owner) then raise exception 'OMS存量商机导入失败:未找到可用的默认商机负责人,请先确认 sys_user 中存在启用用户'; end if; end $$; do $$ begin if to_regclass('public.sys_dict_item') is not null then execute $sql$ update tmp_oms_stage_codes set initial_stage = coalesce(( select s.item_value from public.sys_dict_item s where s.type_code = 'sj_xmjd' and s.item_value = 'S0' and coalesce(s.status, 1) = 1 and coalesce(s.is_deleted, 0) = 0 limit 1 ), initial_stage), follow_stage = coalesce(( select s.item_value from public.sys_dict_item s where s.type_code = 'sj_xmjd' and s.item_value = 'S4A' and coalesce(s.status, 1) = 1 and coalesce(s.is_deleted, 0) = 0 limit 1 ), follow_stage) $sql$; execute $sql$ update tmp_oms_dict_values set opportunity_type = coalesce(( select s.item_value from public.sys_dict_item s where s.type_code = 'sj_jslx' and s.item_label = '新建' and coalesce(s.status, 1) = 1 and coalesce(s.is_deleted, 0) = 0 order by s.sort_order asc nulls last limit 1 ), opportunity_type) $sql$; end if; if to_regclass('public.cnarea') is not null then execute $sql$ update tmp_oms_area_map m set normalized_area = coalesce(( select c.name from public.cnarea c where c.level = 1 and ( c.name = m.raw_area or c.short_name = m.raw_area or replace(c.name, '省', '') = m.raw_area or replace(replace(replace(replace(replace(c.name, '市', ''), '自治区', ''), '壮族', ''), '回族', ''), '维吾尔', '') = m.raw_area ) order by c.area_code asc, c.id asc limit 1 ), m.normalized_area) $sql$; end if; end $$; with customer_source as ( select distinct on (t.customer_name, d.owner_user_id) 'OMS-CUS-' || substr(md5(btrim(t.customer_name) || ':' || d.owner_user_id::text), 1, 16) as customer_code, t.customer_name, t.industry, coalesce(am.normalized_area, t.province) as province, d.owner_user_id from tmp_oms_opportunity_import t left join tmp_oms_area_map am on am.raw_area = t.province cross join tmp_oms_default_owner d order by t.customer_name, d.owner_user_id, t.row_no ) insert into crm_customer ( customer_code, customer_name, customer_type, industry, province, owner_user_id, source, status, remark, created_at, updated_at ) select cs.customer_code, cs.customer_name, null, cs.industry, cs.province, cs.owner_user_id, 'OMS存量导入', 'following', '来自 OMS 存量项目空间(研发转录CRM)0423', now(), now() from customer_source cs where not exists ( select 1 from crm_customer c where btrim(c.customer_name) = btrim(cs.customer_name) and (c.owner_user_id = cs.owner_user_id or c.owner_user_id is null) ) on conflict (customer_code) do update set customer_name = excluded.customer_name, industry = coalesce(nullif(crm_customer.industry, ''), excluded.industry), province = coalesce(nullif(crm_customer.province, ''), excluded.province), owner_user_id = coalesce(crm_customer.owner_user_id, excluded.owner_user_id), source = coalesce(nullif(crm_customer.source, ''), excluded.source), updated_at = now(); insert into crm_opportunity ( opportunity_code, opportunity_name, customer_id, owner_user_id, project_location, operator_name, amount, expected_close_date, confidence_pct, stage, opportunity_type, product_type, source, sales_expansion_id, channel_expansion_id, pre_sales_id, pre_sales_name, competitor_name, archived, archived_at, pushed_to_oms, oms_push_time, description, status, created_at, updated_at ) select t.project_id, t.project_name, customer_match.id, d.owner_user_id, coalesce(am.normalized_area, t.province), null, t.amount, t.order_date, t.confidence_pct, case when t.project_status = '已下单' then 'won' when t.project_status = '重点跟进' then sc.follow_stage else sc.initial_stage end, dv.opportunity_type, 'VDI云桌面', 'OMS存量导入', null, null, t.pre_sales_id, t.pre_sales_name, null, t.project_status = '已下单', case when t.project_status = '已下单' then coalesce(t.order_date::timestamptz, now()) else null end, true, now(), concat_ws(E'\n', 'OMS存量项目空间(研发转录CRM)0423导入', '原项目状态:' || t.project_status, '原项目ID:' || t.project_id ), case when t.project_status = '已下单' then 'won' else 'active' end, now(), now() from tmp_oms_opportunity_import t left join tmp_oms_area_map am on am.raw_area = t.province cross join tmp_oms_default_owner d cross join tmp_oms_stage_codes sc cross join tmp_oms_dict_values dv join lateral ( select c.id from crm_customer c where c.customer_code = 'OMS-CUS-' || substr(md5(btrim(t.customer_name) || ':' || d.owner_user_id::text), 1, 16) or ( btrim(c.customer_name) = btrim(t.customer_name) and (c.owner_user_id = d.owner_user_id or c.owner_user_id is null) ) order by case when c.customer_code = 'OMS-CUS-' || substr(md5(btrim(t.customer_name) || ':' || d.owner_user_id::text), 1, 16) then 0 when c.owner_user_id = d.owner_user_id then 1 else 2 end, c.id limit 1 ) customer_match on true on conflict (opportunity_code) do update set opportunity_name = excluded.opportunity_name, customer_id = excluded.customer_id, owner_user_id = excluded.owner_user_id, project_location = excluded.project_location, amount = excluded.amount, expected_close_date = excluded.expected_close_date, confidence_pct = excluded.confidence_pct, stage = excluded.stage, opportunity_type = excluded.opportunity_type, product_type = excluded.product_type, source = excluded.source, pre_sales_id = excluded.pre_sales_id, pre_sales_name = excluded.pre_sales_name, archived = excluded.archived, archived_at = excluded.archived_at, pushed_to_oms = excluded.pushed_to_oms, oms_push_time = coalesce(crm_opportunity.oms_push_time, excluded.oms_push_time), description = excluded.description, status = excluded.status, updated_at = now(); select count(*) as imported_rows, count(*) filter (where project_status = '已下单') as won_rows, count(*) filter (where project_status <> '已下单') as active_rows, sum(amount) as total_amount from tmp_oms_opportunity_import; select owner_user_id as default_owner_user_id, owner_source from tmp_oms_default_owner;