953 lines
38 KiB
PL/PgSQL
953 lines
38 KiB
PL/PgSQL
-- 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 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 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)
|
|
) 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;
|