-- PostgreSQL 17 full initialization script for public schema -- Usage: -- psql -d your_database -f sql/init_full_pg17.sql -- -- Structure: -- 1) base schema objects -- 2) indexes / triggers / comments -- 3) compatibility absorption for old environments -- -- Notes: -- - This is the current canonical initialization entry. -- - Historical incremental scripts are archived under sql/archive/. begin; set search_path to public; -- ===================================================================== -- Section 1. Utilities -- ===================================================================== -- Unified trigger function for updated_at maintenance. create or replace function set_updated_at() returns trigger language plpgsql as $$ begin new.updated_at = now(); return new; end; $$; create or replace function create_trigger_if_not_exists(trigger_name text, table_name text) returns void language plpgsql as $$ begin if not exists ( select 1 from pg_trigger t join pg_class c on c.oid = t.tgrelid join pg_namespace n on n.oid = c.relnamespace where t.tgname = trigger_name and c.relname = table_name and n.nspname = current_schema() ) then execute format( 'create trigger %I before update on %I for each row execute function set_updated_at()', trigger_name, table_name ); end if; end; $$; create or replace function comment_on_column_if_exists(p_table_name text, p_column_name text, p_comment_text text) returns void language plpgsql as $$ begin if exists ( select 1 from information_schema.columns c where c.table_schema = current_schema() and c.table_name = p_table_name and c.column_name = p_column_name ) then execute format( 'comment on column %I.%I is %L', p_table_name, p_column_name, p_comment_text ); end if; end; $$; -- ===================================================================== -- Section 2. Base tables -- ===================================================================== create table if not exists sys_user ( id bigint generated by default as identity primary key, user_code varchar(50), username varchar(50) not null, real_name varchar(50) not null, mobile varchar(20), email varchar(100), org_id bigint, job_title varchar(100), status smallint not null default 1, hire_date date, avatar_url varchar(255), password_hash varchar(255), created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint uk_sys_user_username unique (username), constraint uk_sys_user_mobile unique (mobile) ); create table if not exists crm_customer ( id bigint generated by default as identity primary key, customer_code varchar(50), customer_name varchar(200) not null, customer_type varchar(50), industry varchar(50), province varchar(50), city varchar(50), address varchar(255), owner_user_id bigint, source varchar(50), status varchar(30) not null default 'potential' check (status in ('potential', 'following', 'won', 'lost')), remark text, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint uk_crm_customer_code unique (customer_code) ); create table if not exists crm_opportunity ( id bigint generated by default as identity primary key, opportunity_code varchar(50) not null, opportunity_name varchar(200) not null, customer_id bigint not null, owner_user_id bigint not null, sales_expansion_id bigint, channel_expansion_id bigint, pre_sales_id bigint, pre_sales_name varchar(100), project_location varchar(100), operator_name varchar(100), amount numeric(18, 2) not null default 0, expected_close_date date, confidence_pct varchar(1) not null default 'C' check (confidence_pct in ('A', 'B', 'C')), stage varchar(50) not null default 'initial_contact', opportunity_type varchar(50), product_type varchar(100), source varchar(50), competitor_name varchar(200), archived boolean not null default false, pushed_to_oms boolean not null default false, oms_push_time timestamptz, description text, status varchar(30) not null default 'active' check (status in ('active', 'won', 'lost', 'closed')), created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint uk_crm_opportunity_code unique (opportunity_code), constraint fk_crm_opportunity_customer foreign key (customer_id) references crm_customer(id) ); create table if not exists crm_opportunity_followup ( id bigint generated by default as identity primary key, opportunity_id bigint not null, followup_time timestamptz not null, followup_type varchar(50) not null, content text not null, next_action varchar(255), followup_user_id bigint not null, source_type varchar(30), source_id bigint, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint fk_crm_opportunity_followup_opportunity foreign key (opportunity_id) references crm_opportunity(id) on delete cascade ); create table if not exists crm_sales_expansion ( id bigint generated by default as identity primary key, employee_no varchar(50) not null, candidate_name varchar(50) not null, office_name varchar(100), mobile varchar(20), email varchar(100), target_dept varchar(100), industry varchar(50), title varchar(100), intent_level varchar(20) not null default 'medium' check (intent_level in ('high', 'medium', 'low')), stage varchar(50) not null default 'initial_contact', has_desktop_exp boolean not null default false, in_progress boolean not null default true, employment_status varchar(20) not null default 'active' check (employment_status in ('active', 'left', 'joined', 'abandoned')), expected_join_date date, owner_user_id bigint not null, remark text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists crm_channel_expansion ( id bigint generated by default as identity primary key, channel_code varchar(50), province varchar(50), city varchar(50), channel_name varchar(200) not null, office_address varchar(255), channel_industry varchar(100), certification_level varchar(100), annual_revenue numeric(18, 2), staff_size integer check (staff_size is null or staff_size >= 0), contact_established_date date, intent_level varchar(20) not null default 'medium' check (intent_level in ('high', 'medium', 'low')), has_desktop_exp boolean not null default false, contact_name varchar(50), contact_title varchar(100), contact_mobile varchar(20), channel_attribute varchar(100), internal_attribute varchar(100), stage varchar(50) not null default 'initial_contact', landed_flag boolean not null default false, expected_sign_date date, owner_user_id bigint not null, remark text, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists crm_channel_expansion_contact ( id bigint generated by default as identity primary key, channel_expansion_id bigint not null, contact_name varchar(50), contact_mobile varchar(20), contact_title varchar(100), sort_order integer not null default 1, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint fk_crm_channel_expansion_contact_channel foreign key (channel_expansion_id) references crm_channel_expansion(id) on delete cascade ); create table if not exists crm_expansion_followup ( id bigint generated by default as identity primary key, biz_type varchar(20) not null check (biz_type in ('sales', 'channel')), biz_id bigint not null, followup_time timestamptz not null, followup_type varchar(50) not null, content text not null, next_action varchar(255), followup_user_id bigint not null, visit_start_time timestamptz, evaluation_content text, next_plan text, source_type varchar(30), source_id bigint, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists work_checkin ( id bigint generated by default as identity primary key, user_id bigint not null, checkin_date date not null, checkin_time timestamptz not null, biz_type varchar(20) check (biz_type is null or biz_type in ('sales', 'channel', 'opportunity')), biz_id bigint, biz_name varchar(200), longitude numeric(10, 6), latitude numeric(10, 6), location_text varchar(255) not null, remark varchar(500), user_name varchar(100), dept_name varchar(200), status varchar(30) not null default 'normal' check (status in ('normal', 'abnormal', 'reissue')), created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists work_daily_report ( id bigint generated by default as identity primary key, user_id bigint not null, report_date date not null, work_content text, tomorrow_plan text, source_type varchar(30) not null default 'manual' check (source_type in ('manual', 'voice')), submit_time timestamptz, status varchar(30) not null default 'draft' check (status in ('draft', 'submitted', 'read', 'reviewed')), score integer check (score is null or score between 0 and 100), created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint uk_work_daily_report_user_date unique (user_id, report_date) ); create table if not exists work_daily_report_comment ( id bigint generated by default as identity primary key, report_id bigint not null, reviewer_user_id bigint not null, score integer check (score is null or score between 0 and 100), comment_content text, reviewed_at timestamptz not null default now(), created_at timestamptz not null default now(), constraint fk_work_daily_report_comment_report foreign key (report_id) references work_daily_report(id) on delete cascade ); create table if not exists work_todo ( id bigint generated by default as identity primary key, user_id bigint not null, title varchar(200) not null, biz_type varchar(30) not null default 'other' check (biz_type in ('opportunity', 'expansion', 'report', 'other')), biz_id bigint, due_date timestamptz, status varchar(20) not null default 'todo' check (status in ('todo', 'done', 'canceled')), priority varchar(20) not null default 'medium' check (priority in ('high', 'medium', 'low')), created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); create table if not exists sys_activity_log ( id bigint generated by default as identity primary key, biz_type varchar(30) not null, biz_id bigint, action_type varchar(50) not null, title varchar(200) not null, content varchar(500), operator_user_id bigint, created_at timestamptz not null default now() ); -- ===================================================================== -- Section 3. Indexes / triggers / comments -- ===================================================================== do $$ begin if not exists ( select 1 from pg_constraint where conname = 'fk_crm_opportunity_channel_expansion' ) then alter table crm_opportunity add constraint fk_crm_opportunity_channel_expansion foreign key (channel_expansion_id) references crm_channel_expansion(id); end if; end $$; create index if not exists idx_crm_customer_owner on crm_customer(owner_user_id); create index if not exists idx_crm_customer_name on crm_customer(customer_name); do $$ begin if exists ( select 1 from information_schema.columns where table_schema = current_schema() and table_name = 'sys_user' and column_name = 'org_id' ) then execute 'create index if not exists idx_sys_user_org_id on sys_user(org_id)'; end if; end; $$; create unique index if not exists uk_crm_sales_expansion_owner_employee_no on crm_sales_expansion(owner_user_id, employee_no); create sequence if not exists crm_channel_expansion_code_seq start with 1 increment by 1 minvalue 1; create index if not exists idx_crm_opportunity_customer on crm_opportunity(customer_id); create index if not exists idx_crm_opportunity_owner on crm_opportunity(owner_user_id); create index if not exists idx_crm_opportunity_sales_expansion on crm_opportunity(sales_expansion_id); create index if not exists idx_crm_opportunity_stage on crm_opportunity(stage); create index if not exists idx_crm_opportunity_expected_close on crm_opportunity(expected_close_date); create index if not exists idx_crm_opportunity_archived on crm_opportunity(archived); create index if not exists idx_crm_opportunity_followup_opportunity_time on crm_opportunity_followup(opportunity_id, followup_time desc); create index if not exists idx_crm_opportunity_followup_user on crm_opportunity_followup(followup_user_id); create index if not exists idx_crm_sales_expansion_owner on crm_sales_expansion(owner_user_id); create index if not exists idx_crm_sales_expansion_stage on crm_sales_expansion(stage); create index if not exists idx_crm_sales_expansion_mobile on crm_sales_expansion(mobile); create index if not exists idx_crm_channel_expansion_owner on crm_channel_expansion(owner_user_id); create index if not exists idx_crm_channel_expansion_stage on crm_channel_expansion(stage); create index if not exists idx_crm_channel_expansion_name on crm_channel_expansion(channel_name); create unique index if not exists uk_crm_channel_expansion_code on crm_channel_expansion(channel_code) where channel_code is not null; create index if not exists idx_crm_channel_expansion_contact_channel on crm_channel_expansion_contact(channel_expansion_id); create index if not exists idx_crm_opportunity_channel_expansion on crm_opportunity(channel_expansion_id); create index if not exists idx_crm_expansion_followup_biz_time on crm_expansion_followup(biz_type, biz_id, followup_time desc); create index if not exists idx_crm_expansion_followup_user on crm_expansion_followup(followup_user_id); create index if not exists idx_work_checkin_user_date on work_checkin(user_id, checkin_date desc); create index if not exists idx_work_daily_report_user_date on work_daily_report(user_id, report_date desc); create index if not exists idx_work_daily_report_status on work_daily_report(status); create index if not exists idx_work_daily_report_comment_report on work_daily_report_comment(report_id); create index if not exists idx_sys_activity_log_created on sys_activity_log(created_at desc); create index if not exists idx_sys_activity_log_biz on sys_activity_log(biz_type, biz_id); select create_trigger_if_not_exists('trg_sys_user_updated_at', 'sys_user'); select create_trigger_if_not_exists('trg_crm_customer_updated_at', 'crm_customer'); select create_trigger_if_not_exists('trg_crm_opportunity_updated_at', 'crm_opportunity'); select create_trigger_if_not_exists('trg_crm_opportunity_followup_updated_at', 'crm_opportunity_followup'); select create_trigger_if_not_exists('trg_crm_sales_expansion_updated_at', 'crm_sales_expansion'); select create_trigger_if_not_exists('trg_crm_channel_expansion_updated_at', 'crm_channel_expansion'); select create_trigger_if_not_exists('trg_crm_channel_expansion_contact_updated_at', 'crm_channel_expansion_contact'); select create_trigger_if_not_exists('trg_crm_expansion_followup_updated_at', 'crm_expansion_followup'); select create_trigger_if_not_exists('trg_work_checkin_updated_at', 'work_checkin'); select create_trigger_if_not_exists('trg_work_daily_report_updated_at', 'work_daily_report'); select create_trigger_if_not_exists('trg_work_todo_updated_at', 'work_todo'); comment on table sys_user is '系统用户'; comment on table crm_customer is '客户主表'; comment on table crm_opportunity is '商机主表'; comment on table crm_opportunity_followup is '商机跟进记录'; comment on table crm_sales_expansion is '销售人员拓展'; comment on table crm_channel_expansion is '渠道拓展'; comment on table crm_channel_expansion_contact is '渠道拓展联系人'; comment on table crm_expansion_followup is '拓展跟进记录'; comment on table work_checkin is '外勤打卡'; comment on table work_daily_report is '日报'; comment on table work_daily_report_comment is '日报点评'; comment on table work_todo is '待办事项'; comment on table sys_activity_log is '首页动态日志'; commit; -- ===================================================================== -- Compatibility DDL section -- Purpose: -- 1) make this script usable for both fresh installs and old-environment upgrades -- 2) absorb historical DDL migration scripts into a single deployment entry -- 3) keep base-framework tables managed by unisbase itself -- -- Base framework dependencies not created here: -- sys_org, sys_dict_item, sys_tenant_user, sys_role, sys_user_role ... -- ===================================================================== begin; set search_path to public; -- sys_user compatibility: dept_id -> org_id DO $$ BEGIN IF to_regclass('public.sys_user') IS NOT NULL THEN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'sys_user' AND column_name = 'org_id' ) THEN ALTER TABLE public.sys_user ADD COLUMN org_id bigint; END IF; IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'sys_user' AND column_name = 'dept_id' ) THEN EXECUTE 'update public.sys_user set org_id = coalesce(org_id, dept_id) where dept_id is not null'; ALTER TABLE public.sys_user DROP COLUMN dept_id; END IF; END IF; END $$; DROP INDEX IF EXISTS public.idx_sys_user_dept_id; CREATE INDEX IF NOT EXISTS idx_sys_user_org_id ON public.sys_user(org_id); -- crm_sales_expansion compatibility: ensure employee_no / office_name / target_dept text ALTER TABLE IF EXISTS crm_sales_expansion ADD COLUMN IF NOT EXISTS employee_no varchar(50), ADD COLUMN IF NOT EXISTS office_name varchar(100), ADD COLUMN IF NOT EXISTS target_dept varchar(100); DO $$ BEGIN IF to_regclass('public.crm_sales_expansion') IS NOT NULL THEN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'crm_sales_expansion' AND column_name = 'target_dept_id' ) THEN UPDATE crm_sales_expansion s SET target_dept = COALESCE( NULLIF(s.target_dept, ''), ( SELECT d.item_label FROM sys_dict_item d WHERE d.type_code = 'tz_ssbm' AND d.item_value = s.target_dept_id::varchar AND d.status = 1 AND COALESCE(d.is_deleted, 0) = 0 ORDER BY d.sort_order ASC NULLS LAST, d.dict_item_id ASC LIMIT 1 ), ( SELECT o.org_name FROM sys_org o WHERE o.id = s.target_dept_id LIMIT 1 ), s.target_dept_id::varchar ) WHERE s.target_dept_id IS NOT NULL; ALTER TABLE crm_sales_expansion DROP COLUMN target_dept_id; END IF; UPDATE crm_sales_expansion SET employee_no = concat('EMP', lpad(id::text, 6, '0')) WHERE employee_no IS NULL OR btrim(employee_no) = ''; WITH duplicated AS ( SELECT id, row_number() over (partition by owner_user_id, employee_no order by id asc) AS rn FROM crm_sales_expansion WHERE employee_no IS NOT NULL AND btrim(employee_no) <> '' ) UPDATE crm_sales_expansion s SET employee_no = concat(s.employee_no, '-', s.id) FROM duplicated d WHERE s.id = d.id AND d.rn > 1; IF NOT EXISTS ( SELECT 1 FROM crm_sales_expansion WHERE employee_no IS NULL OR btrim(employee_no) = '' ) THEN ALTER TABLE crm_sales_expansion ALTER COLUMN employee_no SET NOT NULL; END IF; END IF; END $$; DO $$ BEGIN IF to_regclass('public.crm_sales_expansion') IS NOT NULL THEN IF NOT EXISTS ( SELECT 1 FROM pg_indexes WHERE schemaname = 'public' AND indexname = 'uk_crm_sales_expansion_owner_employee_no' ) THEN CREATE UNIQUE INDEX uk_crm_sales_expansion_owner_employee_no ON crm_sales_expansion(owner_user_id, employee_no); END IF; END IF; END $$; -- crm_opportunity compatibility: absorb old extension fields and relationships ALTER TABLE IF EXISTS crm_opportunity ADD COLUMN IF NOT EXISTS sales_expansion_id bigint, ADD COLUMN IF NOT EXISTS channel_expansion_id bigint, ADD COLUMN IF NOT EXISTS pre_sales_id bigint, ADD COLUMN IF NOT EXISTS pre_sales_name varchar(100), ADD COLUMN IF NOT EXISTS project_location varchar(100), ADD COLUMN IF NOT EXISTS operator_name varchar(100), ADD COLUMN IF NOT EXISTS competitor_name varchar(200); DO $$ BEGIN IF to_regclass('public.crm_opportunity') IS NOT NULL THEN ALTER TABLE public.crm_opportunity DROP CONSTRAINT IF EXISTS crm_opportunity_stage_check; IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'fk_crm_opportunity_sales_expansion' ) THEN ALTER TABLE public.crm_opportunity ADD CONSTRAINT fk_crm_opportunity_sales_expansion FOREIGN KEY (sales_expansion_id) REFERENCES public.crm_sales_expansion(id); END IF; IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'fk_crm_opportunity_channel_expansion' ) THEN ALTER TABLE public.crm_opportunity ADD CONSTRAINT fk_crm_opportunity_channel_expansion FOREIGN KEY (channel_expansion_id) REFERENCES public.crm_channel_expansion(id); END IF; END IF; END $$; -- crm_channel_expansion compatibility: absorb detail columns and contact sub-table ALTER TABLE IF EXISTS crm_channel_expansion ADD COLUMN IF NOT EXISTS channel_code varchar(50), ADD COLUMN IF NOT EXISTS office_address varchar(255), ADD COLUMN IF NOT EXISTS channel_industry varchar(100), ADD COLUMN IF NOT EXISTS city varchar(50), ADD COLUMN IF NOT EXISTS certification_level varchar(100), ADD COLUMN IF NOT EXISTS contact_established_date date, ADD COLUMN IF NOT EXISTS intent_level varchar(20), ADD COLUMN IF NOT EXISTS has_desktop_exp boolean, ADD COLUMN IF NOT EXISTS channel_attribute varchar(100), ADD COLUMN IF NOT EXISTS internal_attribute varchar(100); DO $$ BEGIN IF to_regclass('public.crm_channel_expansion') IS NOT NULL THEN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'crm_channel_expansion' AND column_name = 'industry' ) THEN EXECUTE 'update public.crm_channel_expansion set channel_industry = coalesce(channel_industry, industry) where channel_industry is null and industry is not null'; END IF; UPDATE crm_channel_expansion SET intent_level = COALESCE(intent_level, 'medium'), has_desktop_exp = COALESCE(has_desktop_exp, false); ALTER TABLE crm_channel_expansion ALTER COLUMN intent_level SET DEFAULT 'medium', ALTER COLUMN has_desktop_exp SET DEFAULT false; IF EXISTS ( SELECT 1 FROM crm_channel_expansion WHERE intent_level IS NULL ) THEN RAISE NOTICE 'crm_channel_expansion.intent_level still has null values before not-null enforcement'; ELSE ALTER TABLE crm_channel_expansion ALTER COLUMN intent_level SET NOT NULL; END IF; IF EXISTS ( SELECT 1 FROM crm_channel_expansion WHERE has_desktop_exp IS NULL ) THEN RAISE NOTICE 'crm_channel_expansion.has_desktop_exp still has null values before not-null enforcement'; ELSE ALTER TABLE crm_channel_expansion ALTER COLUMN has_desktop_exp SET NOT NULL; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'crm_channel_expansion_intent_level_check' ) THEN ALTER TABLE crm_channel_expansion ADD CONSTRAINT crm_channel_expansion_intent_level_check CHECK (intent_level IN ('high', 'medium', 'low')); END IF; END IF; END $$; CREATE TABLE IF NOT EXISTS crm_channel_expansion_contact ( id bigint generated by default as identity primary key, channel_expansion_id bigint not null, contact_name varchar(50), contact_mobile varchar(20), contact_title varchar(100), sort_order integer not null default 1, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint fk_crm_channel_expansion_contact_channel foreign key (channel_expansion_id) references crm_channel_expansion(id) on delete cascade ); DO $$ BEGIN IF to_regclass('public.crm_channel_expansion') IS NOT NULL AND to_regclass('public.crm_channel_expansion_contact') IS NOT NULL THEN INSERT INTO crm_channel_expansion_contact ( channel_expansion_id, contact_name, contact_mobile, contact_title, sort_order, created_at, updated_at ) SELECT c.id, c.contact_name, c.contact_mobile, c.contact_title, 1, now(), now() FROM crm_channel_expansion c WHERE (COALESCE(btrim(c.contact_name), '') <> '' OR COALESCE(btrim(c.contact_mobile), '') <> '' OR COALESCE(btrim(c.contact_title), '') <> '') AND NOT EXISTS ( SELECT 1 FROM crm_channel_expansion_contact cc WHERE cc.channel_expansion_id = c.id ); END IF; END $$; -- follow-up compatibility: source fields and structured work-report fields ALTER TABLE IF EXISTS crm_expansion_followup ADD COLUMN IF NOT EXISTS visit_start_time timestamptz, ADD COLUMN IF NOT EXISTS evaluation_content text, ADD COLUMN IF NOT EXISTS next_plan text, ADD COLUMN IF NOT EXISTS source_type varchar(30), ADD COLUMN IF NOT EXISTS source_id bigint; ALTER TABLE IF EXISTS crm_opportunity_followup ADD COLUMN IF NOT EXISTS source_type varchar(30), ADD COLUMN IF NOT EXISTS source_id bigint; -- work_checkin compatibility: relation fields ALTER TABLE IF EXISTS work_checkin ADD COLUMN IF NOT EXISTS biz_type varchar(20), ADD COLUMN IF NOT EXISTS biz_id bigint, ADD COLUMN IF NOT EXISTS biz_name varchar(200), ADD COLUMN IF NOT EXISTS user_name varchar(100), ADD COLUMN IF NOT EXISTS dept_name varchar(200); DO $$ BEGIN IF to_regclass('public.work_checkin') IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conrelid = 'public.work_checkin'::regclass AND conname = 'work_checkin_biz_type_check' ) THEN ALTER TABLE public.work_checkin ADD CONSTRAINT work_checkin_biz_type_check CHECK (biz_type IS NULL OR biz_type IN ('sales', 'channel', 'opportunity')); END IF; END $$; -- additional indexes absorbed from historical DDLs CREATE INDEX IF NOT EXISTS idx_crm_opportunity_channel_expansion ON crm_opportunity(channel_expansion_id); CREATE INDEX IF NOT EXISTS idx_crm_expansion_followup_source ON crm_expansion_followup(source_type, source_id); CREATE INDEX IF NOT EXISTS idx_crm_opportunity_followup_source ON crm_opportunity_followup(source_type, source_id); CREATE INDEX IF NOT EXISTS idx_crm_channel_expansion_contact_channel ON crm_channel_expansion_contact(channel_expansion_id); -- Column comments WITH column_comments(table_name, column_name, comment_text) AS ( VALUES ('sys_user', 'id', '用户主键'), ('sys_user', 'user_id', '用户ID'), ('sys_user', 'user_code', '工号/员工编号'), ('sys_user', 'username', '登录账号'), ('sys_user', 'real_name', '姓名'), ('sys_user', 'display_name', '显示名称'), ('sys_user', 'mobile', '手机号'), ('sys_user', 'phone', '手机号'), ('sys_user', 'email', '邮箱'), ('sys_user', 'org_id', '所属组织ID'), ('sys_user', 'job_title', '职位'), ('sys_user', 'status', '用户状态'), ('sys_user', 'hire_date', '入职日期'), ('sys_user', 'avatar_url', '头像地址'), ('sys_user', 'password_hash', '密码哈希'), ('sys_user', 'created_at', '创建时间'), ('sys_user', 'updated_at', '更新时间'), ('sys_user', 'is_deleted', '逻辑删除标记'), ('sys_user', 'pwd_reset_required', '首次登录是否需要重置密码'), ('sys_user', 'is_platform_admin', '是否平台管理员'), ('crm_customer', 'id', '客户主键'), ('crm_customer', 'customer_code', '客户编码'), ('crm_customer', 'customer_name', '客户名称'), ('crm_customer', 'customer_type', '客户类型'), ('crm_customer', 'industry', '行业'), ('crm_customer', 'province', '省份'), ('crm_customer', 'city', '城市'), ('crm_customer', 'address', '详细地址'), ('crm_customer', 'owner_user_id', '当前负责人ID'), ('crm_customer', 'source', '客户来源'), ('crm_customer', 'status', '客户状态'), ('crm_customer', 'remark', '备注说明'), ('crm_customer', 'created_at', '创建时间'), ('crm_customer', 'updated_at', '更新时间'), ('crm_opportunity', 'id', '商机主键'), ('crm_opportunity', 'opportunity_code', '商机编号'), ('crm_opportunity', 'opportunity_name', '商机名称'), ('crm_opportunity', 'customer_id', '客户ID'), ('crm_opportunity', 'owner_user_id', '商机负责人ID'), ('crm_opportunity', 'sales_expansion_id', '关联销售拓展ID'), ('crm_opportunity', 'channel_expansion_id', '关联渠道拓展ID'), ('crm_opportunity', 'pre_sales_id', '售前ID'), ('crm_opportunity', 'pre_sales_name', '售前姓名'), ('crm_opportunity', 'project_location', '项目所在地'), ('crm_opportunity', 'operator_name', '运作方'), ('crm_opportunity', 'amount', '商机金额'), ('crm_opportunity', 'expected_close_date', '预计结单日期'), ('crm_opportunity', 'confidence_pct', '把握度等级(A/B/C)'), ('crm_opportunity', 'stage', '商机阶段'), ('crm_opportunity', 'opportunity_type', '商机类型'), ('crm_opportunity', 'product_type', '产品类型'), ('crm_opportunity', 'source', '商机来源'), ('crm_opportunity', 'competitor_name', '竞品名称'), ('crm_opportunity', 'archived', '是否归档'), ('crm_opportunity', 'pushed_to_oms', '是否已推送OMS'), ('crm_opportunity', 'oms_push_time', '推送OMS时间'), ('crm_opportunity', 'description', '商机说明/备注'), ('crm_opportunity', 'status', '商机状态'), ('crm_opportunity', 'created_at', '创建时间'), ('crm_opportunity', 'updated_at', '更新时间'), ('crm_opportunity_followup', 'id', '跟进记录主键'), ('crm_opportunity_followup', 'opportunity_id', '商机ID'), ('crm_opportunity_followup', 'followup_time', '跟进时间'), ('crm_opportunity_followup', 'followup_type', '跟进方式'), ('crm_opportunity_followup', 'content', '跟进内容'), ('crm_opportunity_followup', 'next_action', '下一步动作'), ('crm_opportunity_followup', 'followup_user_id', '跟进人ID'), ('crm_opportunity_followup', 'source_type', '来源类型'), ('crm_opportunity_followup', 'source_id', '来源记录ID'), ('crm_opportunity_followup', 'created_at', '创建时间'), ('crm_opportunity_followup', 'updated_at', '更新时间'), ('crm_sales_expansion', 'id', '销售拓展主键'), ('crm_sales_expansion', 'employee_no', '工号/员工编号'), ('crm_sales_expansion', 'candidate_name', '候选人姓名'), ('crm_sales_expansion', 'office_name', '办事处/代表处'), ('crm_sales_expansion', 'mobile', '手机号'), ('crm_sales_expansion', 'email', '邮箱'), ('crm_sales_expansion', 'target_dept', '所属部门'), ('crm_sales_expansion', 'industry', '所属行业'), ('crm_sales_expansion', 'title', '职务'), ('crm_sales_expansion', 'intent_level', '合作意向'), ('crm_sales_expansion', 'stage', '跟进阶段'), ('crm_sales_expansion', 'has_desktop_exp', '是否有云桌面经验'), ('crm_sales_expansion', 'in_progress', '是否持续跟进中'), ('crm_sales_expansion', 'employment_status', '候选人状态'), ('crm_sales_expansion', 'expected_join_date', '预计入职日期'), ('crm_sales_expansion', 'owner_user_id', '负责人ID'), ('crm_sales_expansion', 'remark', '备注说明'), ('crm_sales_expansion', 'created_at', '创建时间'), ('crm_sales_expansion', 'updated_at', '更新时间'), ('crm_channel_expansion', 'id', '渠道拓展主键'), ('crm_channel_expansion', 'channel_code', '渠道编码'), ('crm_channel_expansion', 'province', '省份'), ('crm_channel_expansion', 'city', '市'), ('crm_channel_expansion', 'channel_name', '渠道名称'), ('crm_channel_expansion', 'office_address', '办公地址'), ('crm_channel_expansion', 'channel_industry', '聚焦行业'), ('crm_channel_expansion', 'certification_level', '认证级别'), ('crm_channel_expansion', 'industry', '行业(兼容旧字段)'), ('crm_channel_expansion', 'annual_revenue', '年营收'), ('crm_channel_expansion', 'staff_size', '人员规模'), ('crm_channel_expansion', 'contact_established_date', '建立联系日期'), ('crm_channel_expansion', 'intent_level', '合作意向'), ('crm_channel_expansion', 'has_desktop_exp', '是否有云桌面经验'), ('crm_channel_expansion', 'contact_name', '主联系人姓名(兼容旧结构)'), ('crm_channel_expansion', 'contact_title', '主联系人职务(兼容旧结构)'), ('crm_channel_expansion', 'contact_mobile', '主联系人电话(兼容旧结构)'), ('crm_channel_expansion', 'channel_attribute', '渠道属性编码,多个值逗号分隔'), ('crm_channel_expansion', 'internal_attribute', '新华三内部属性编码,多个值逗号分隔'), ('crm_channel_expansion', 'stage', '渠道合作阶段'), ('crm_channel_expansion', 'landed_flag', '是否已落地'), ('crm_channel_expansion', 'expected_sign_date', '预计签约日期'), ('crm_channel_expansion', 'owner_user_id', '负责人ID'), ('crm_channel_expansion', 'remark', '备注说明'), ('crm_channel_expansion', 'created_at', '创建时间'), ('crm_channel_expansion', 'updated_at', '更新时间'), ('crm_channel_expansion_contact', 'id', '联系人主键'), ('crm_channel_expansion_contact', 'channel_expansion_id', '渠道拓展ID'), ('crm_channel_expansion_contact', 'contact_name', '联系人姓名'), ('crm_channel_expansion_contact', 'contact_mobile', '联系人电话'), ('crm_channel_expansion_contact', 'contact_title', '联系人职务'), ('crm_channel_expansion_contact', 'sort_order', '排序号'), ('crm_channel_expansion_contact', 'created_at', '创建时间'), ('crm_channel_expansion_contact', 'updated_at', '更新时间'), ('crm_expansion_followup', 'id', '跟进记录主键'), ('crm_expansion_followup', 'biz_type', '业务类型'), ('crm_expansion_followup', 'biz_id', '业务对象ID'), ('crm_expansion_followup', 'followup_time', '跟进时间'), ('crm_expansion_followup', 'followup_type', '跟进方式'), ('crm_expansion_followup', 'content', '跟进内容'), ('crm_expansion_followup', 'next_action', '下一步动作'), ('crm_expansion_followup', 'followup_user_id', '跟进人ID'), ('crm_expansion_followup', 'visit_start_time', '拜访开始时间'), ('crm_expansion_followup', 'evaluation_content', '评估内容'), ('crm_expansion_followup', 'next_plan', '后续规划'), ('crm_expansion_followup', 'source_type', '来源类型'), ('crm_expansion_followup', 'source_id', '来源记录ID'), ('crm_expansion_followup', 'created_at', '创建时间'), ('crm_expansion_followup', 'updated_at', '更新时间'), ('work_checkin', 'id', '打卡记录主键'), ('work_checkin', 'user_id', '打卡人ID'), ('work_checkin', 'checkin_date', '打卡日期'), ('work_checkin', 'checkin_time', '打卡时间'), ('work_checkin', 'biz_type', '关联对象类型'), ('work_checkin', 'biz_id', '关联对象ID'), ('work_checkin', 'biz_name', '关联对象名称'), ('work_checkin', 'longitude', '经度'), ('work_checkin', 'latitude', '纬度'), ('work_checkin', 'location_text', '打卡地点'), ('work_checkin', 'remark', '备注说明(含现场照片元数据)'), ('work_checkin', 'user_name', '打卡人姓名快照'), ('work_checkin', 'dept_name', '所属部门快照'), ('work_checkin', 'status', '打卡状态'), ('work_checkin', 'created_at', '创建时间'), ('work_checkin', 'updated_at', '更新时间'), ('work_daily_report', 'id', '日报主键'), ('work_daily_report', 'user_id', '提交人ID'), ('work_daily_report', 'report_date', '日报日期'), ('work_daily_report', 'work_content', '今日工作内容(含结构化明细元数据)'), ('work_daily_report', 'tomorrow_plan', '明日工作计划(含结构化计划项元数据)'), ('work_daily_report', 'source_type', '提交来源'), ('work_daily_report', 'submit_time', '提交时间'), ('work_daily_report', 'status', '日报状态'), ('work_daily_report', 'score', '日报评分'), ('work_daily_report', 'created_at', '创建时间'), ('work_daily_report', 'updated_at', '更新时间'), ('work_daily_report_comment', 'id', '点评记录主键'), ('work_daily_report_comment', 'report_id', '日报ID'), ('work_daily_report_comment', 'reviewer_user_id', '点评人ID'), ('work_daily_report_comment', 'score', '点评评分'), ('work_daily_report_comment', 'comment_content', '点评内容'), ('work_daily_report_comment', 'reviewed_at', '点评时间'), ('work_daily_report_comment', 'created_at', '创建时间'), ('work_todo', 'id', '待办主键'), ('work_todo', 'user_id', '所属用户ID'), ('work_todo', 'title', '待办标题'), ('work_todo', 'biz_type', '业务类型'), ('work_todo', 'biz_id', '业务对象ID'), ('work_todo', 'due_date', '截止时间'), ('work_todo', 'status', '待办状态'), ('work_todo', 'priority', '优先级'), ('work_todo', 'created_at', '创建时间'), ('work_todo', 'updated_at', '更新时间'), ('sys_activity_log', 'id', '动态主键'), ('sys_activity_log', 'biz_type', '业务类型'), ('sys_activity_log', 'biz_id', '业务对象ID'), ('sys_activity_log', 'action_type', '动作类型'), ('sys_activity_log', 'title', '动态标题'), ('sys_activity_log', 'content', '动态内容'), ('sys_activity_log', 'operator_user_id', '操作人ID'), ('sys_activity_log', 'created_at', '创建时间') ) SELECT comment_on_column_if_exists(table_name, column_name, comment_text) FROM column_comments; INSERT INTO sys_dict_item (type_code, item_label, item_value, sort_order, status, is_deleted, remark) SELECT v.type_code, v.item_label, v.item_value, v.sort_order, 1, 0, '商机建设类型' FROM ( VALUES ('sj_jslx', '新建', '新建', 1), ('sj_jslx', '扩容', '扩容', 2), ('sj_jslx', '替换', '替换', 3), ('sj_xmbwd', 'A', 'A', 1), ('sj_xmbwd', 'B', 'B', 2), ('sj_xmbwd', 'C', 'C', 3) ) AS v(type_code, item_label, item_value, sort_order) WHERE NOT EXISTS ( SELECT 1 FROM sys_dict_item s WHERE s.type_code = v.type_code AND s.item_value = v.item_value AND COALESCE(s.is_deleted, 0) = 0 ); commit;