unis_crm/sql/init_full_pg17.sql

1030 lines
42 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

-- 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,
actual_signed_amount numeric(18, 2),
is_poc boolean not null default false,
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),
latest_progress text,
next_plan text,
archived boolean not null default false,
archived_at timestamptz,
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_archived_at on crm_opportunity(archived_at);
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),
ADD COLUMN IF NOT EXISTS actual_signed_amount numeric(18, 2),
ADD COLUMN IF NOT EXISTS is_poc boolean not null default false;
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 $$;
ALTER TABLE IF EXISTS crm_opportunity
ADD COLUMN IF NOT EXISTS latest_progress text,
ADD COLUMN IF NOT EXISTS next_plan text;
-- 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);
CREATE TABLE IF NOT EXISTS business_calendar_day (
id bigint generated by default as identity primary key,
calendar_date date not null,
calendar_year integer not null,
day_of_week integer not null,
day_type_code integer not null default 0,
day_type varchar(32) not null default 'WORKDAY',
day_name varchar(64) not null,
holiday_name varchar(100),
holiday_target varchar(100),
holiday_wage integer,
is_weekend boolean not null default false,
is_rest_day boolean not null default false,
is_workday boolean not null default true,
is_holiday boolean not null default false,
is_legal_holiday boolean not null default false,
is_makeup_workday boolean not null default false,
source varchar(50) not null default 'timor',
synced_by_user_id bigint,
synced_at timestamptz not null default now(),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint uk_business_calendar_day_date unique (calendar_date)
);
CREATE INDEX IF NOT EXISTS idx_business_calendar_day_year_date
ON business_calendar_day(calendar_year, calendar_date);
CREATE INDEX IF NOT EXISTS idx_business_calendar_day_type
ON business_calendar_day(day_type, calendar_date);
-- 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', 'actual_signed_amount', '实际签约金额'),
('crm_opportunity', 'is_poc', '是否POC测试项目'),
('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', 'latest_progress', '项目最新进展'),
('crm_opportunity', 'next_plan', '下一步销售计划'),
('crm_opportunity', 'archived', '是否归档'),
('crm_opportunity', 'archived_at', '归档时间'),
('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', '更新时间'),
('business_calendar_day', 'id', '业务日历主键'),
('business_calendar_day', 'calendar_date', '日历日期'),
('business_calendar_day', 'calendar_year', '年份'),
('business_calendar_day', 'day_of_week', '星期1-7 对应周一到周日'),
('business_calendar_day', 'day_type_code', '日历类型编码0工作日、1周末休息、2节假日、3调休补班'),
('business_calendar_day', 'day_type', '日历类型'),
('business_calendar_day', 'day_name', '日历展示名称'),
('business_calendar_day', 'holiday_name', '节假日名称'),
('business_calendar_day', 'holiday_target', '调休关联节日'),
('business_calendar_day', 'holiday_wage', '节假日工资倍数'),
('business_calendar_day', 'is_weekend', '是否周末'),
('business_calendar_day', 'is_rest_day', '是否休息日'),
('business_calendar_day', 'is_workday', '是否工作日'),
('business_calendar_day', 'is_holiday', '是否节假日'),
('business_calendar_day', 'is_legal_holiday', '是否法定节假日'),
('business_calendar_day', 'is_makeup_workday', '是否法定调休补班'),
('business_calendar_day', 'source', '日历来源'),
('business_calendar_day', 'synced_by_user_id', '最近同步人ID'),
('business_calendar_day', 'synced_at', '最近同步时间'),
('business_calendar_day', 'created_at', '创建时间'),
('business_calendar_day', '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;