v0.1.0: CRM/ERP 系统内测版本 - 安全加固完成

- Docker bridge 网络隔离(8000 端口封死)
- Gunicorn 4 Worker 多进程
- Alembic 数据库迁移基线
- 日志轮转 20m×3
- JWT 密钥 + DB 密码 + CORS 收紧
- 3-2-1 备份链路(NAS + R740-B 冷备)
- 连接池 pool_pre_ping + pool_recycle=3600
This commit is contained in:
hankin
2026-03-16 07:31:37 +00:00
commit 423baff73b
2578 changed files with 824643 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
-- ============================================================
-- Phase 3 数据库迁移脚本
-- 执行方式: psql -U postgres -d crm_erp -f migration_phase3.sql
-- ============================================================
-- ────── 1. AI 对话持久化 ──────
CREATE TABLE IF NOT EXISTS ai_chat_sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES sys_users(id),
role VARCHAR(10) NOT NULL CHECK (role IN ('user', 'assistant')),
content TEXT NOT NULL,
msg_type VARCHAR(20) DEFAULT 'text',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE ai_chat_sessions IS 'AI 悬浮球对话历史(按用户分区)';
CREATE INDEX IF NOT EXISTS idx_chat_user_time ON ai_chat_sessions(user_id, created_at DESC);
-- ────── 2. 客户 AI 画像字段 ──────
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'crm_customers' AND column_name = 'ai_persona'
) THEN
ALTER TABLE crm_customers ADD COLUMN ai_persona JSONB DEFAULT '{}';
COMMENT ON COLUMN crm_customers.ai_persona IS 'AI 提炼的客户画像(痛点/偏好/购买习惯等)';
END IF;
END $$;
-- ────── 3. 销售日志表 ──────
CREATE TABLE IF NOT EXISTS sales_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
salesperson_id UUID NOT NULL REFERENCES sys_users(id),
customer_id UUID REFERENCES crm_customers(id),
content TEXT NOT NULL,
log_date DATE DEFAULT CURRENT_DATE,
ai_processed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE sales_logs IS '销售日志表(一源多用数据源)';
CREATE INDEX IF NOT EXISTS idx_slog_sales ON sales_logs(salesperson_id) WHERE is_deleted = FALSE;
CREATE INDEX IF NOT EXISTS idx_slog_cust ON sales_logs(customer_id) WHERE is_deleted = FALSE;
CREATE INDEX IF NOT EXISTS idx_slog_date ON sales_logs(log_date) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_slog_updated
BEFORE UPDATE ON sales_logs
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ────── 4. AI 报告草稿表 ──────
CREATE TABLE IF NOT EXISTS ai_report_drafts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
author_id UUID NOT NULL REFERENCES sys_users(id),
report_type VARCHAR(20) NOT NULL CHECK (report_type IN ('weekly', 'monthly')),
period_start DATE NOT NULL,
period_end DATE NOT NULL,
content_md TEXT NOT NULL,
status VARCHAR(20) DEFAULT 'draft' CHECK (status IN ('draft', 'confirmed', 'archived')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE ai_report_drafts IS 'AI 生成的报告草稿(周报/月报)';
CREATE INDEX IF NOT EXISTS idx_report_author ON ai_report_drafts(author_id) WHERE is_deleted = FALSE;
CREATE INDEX IF NOT EXISTS idx_report_period ON ai_report_drafts(period_start, period_end);
CREATE TRIGGER trg_report_updated
BEFORE UPDATE ON ai_report_drafts
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ────── 完成 ──────
-- 新增: ai_chat_sessions, sales_logs, ai_report_drafts (3 张表)
-- 修改: crm_customers 增加 ai_persona JSONB 字段
+53
View File
@@ -0,0 +1,53 @@
-- ============================================================
-- Phase 4 数据库迁移脚本
-- 执行方式: psql -U postgres -d crm_erp -f migration_phase4.sql
-- ============================================================
-- ────── 1. pg_trgm 扩展(客户模糊搜索) ──────
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- 为客户名称建 pg_trgm GIN 索引
CREATE INDEX IF NOT EXISTS idx_cust_name_trgm
ON crm_customers USING gin (name gin_trgm_ops);
-- ────── 2. 销项发票表 finance_sales_invoices ──────
CREATE TABLE IF NOT EXISTS finance_sales_invoices (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
issuer VARCHAR(200) NOT NULL, -- 开票方/我方主体
receiver_customer_id UUID NOT NULL REFERENCES crm_customers(id), -- 受票方
invoice_number VARCHAR(100) NOT NULL UNIQUE, -- 发票号
amount NUMERIC(14,2) NOT NULL DEFAULT 0, -- 票面金额
billing_date DATE NOT NULL, -- 开票时间
payment_status VARCHAR(20) NOT NULL DEFAULT '未回款'
CHECK (payment_status IN ('未回款', '部分回款', '已结清')),
payment_date DATE, -- 回款时间
payment_amount NUMERIC(14,2) DEFAULT 0, -- 已回款金额
remark TEXT,
created_by UUID REFERENCES sys_users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE finance_sales_invoices IS '销项发票表(AR 应收账款核心)';
COMMENT ON COLUMN finance_sales_invoices.issuer IS '开票方/我方主体名称';
COMMENT ON COLUMN finance_sales_invoices.receiver_customer_id IS '受票方,关联 crm_customers';
COMMENT ON COLUMN finance_sales_invoices.invoice_number IS '发票号码(全局唯一)';
COMMENT ON COLUMN finance_sales_invoices.amount IS '票面金额';
COMMENT ON COLUMN finance_sales_invoices.billing_date IS '开票日期';
COMMENT ON COLUMN finance_sales_invoices.payment_status IS '回款状态: 未回款/部分回款/已结清';
COMMENT ON COLUMN finance_sales_invoices.payment_date IS '回款日期';
COMMENT ON COLUMN finance_sales_invoices.payment_amount IS '已回款金额(用于部分回款场景)';
CREATE INDEX IF NOT EXISTS idx_fsi_receiver ON finance_sales_invoices(receiver_customer_id) WHERE is_deleted = FALSE;
CREATE INDEX IF NOT EXISTS idx_fsi_number ON finance_sales_invoices(invoice_number) WHERE is_deleted = FALSE;
CREATE INDEX IF NOT EXISTS idx_fsi_billing_date ON finance_sales_invoices(billing_date) WHERE is_deleted = FALSE;
CREATE INDEX IF NOT EXISTS idx_fsi_payment_status ON finance_sales_invoices(payment_status) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_fsi_updated
BEFORE UPDATE ON finance_sales_invoices
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ────── 完成 ──────
-- 新增: finance_sales_invoices (1 张表)
-- 新增: pg_trgm 扩展 + crm_customers.name GIN 索引
+42
View File
@@ -0,0 +1,42 @@
-- ═══════════════════════════════════════════════════════════════
-- V5.0 ERP 智能化升级 — 底层数据物理基座重构
-- ═══════════════════════════════════════════════════════════════
-- 1. 创建 crm_contacts 子表 (联系人维度,1:N 映射到 crm_customers)
CREATE TABLE IF NOT EXISTS crm_contacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL REFERENCES crm_customers(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
phone VARCHAR(30),
title VARCHAR(100), -- 职位/头衔
ai_buyer_persona JSONB DEFAULT '{}'::jsonb, -- 联系人级 AI 画像
is_deleted BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE crm_contacts IS '客户联系人子表 (V5.0)';
COMMENT ON COLUMN crm_contacts.customer_id IS '关联的客户 UUID';
COMMENT ON COLUMN crm_contacts.name IS '联系人姓名';
COMMENT ON COLUMN crm_contacts.phone IS '联系人电话';
COMMENT ON COLUMN crm_contacts.title IS '职位/头衔';
COMMENT ON COLUMN crm_contacts.ai_buyer_persona IS '联系人个体画像 JSONBrole / kpi / preference';
-- 索引
CREATE INDEX IF NOT EXISTS idx_contacts_customer ON crm_contacts(customer_id);
CREATE INDEX IF NOT EXISTS idx_contacts_not_deleted ON crm_contacts(customer_id) WHERE is_deleted = FALSE;
-- updated_at 自动更新触发器
CREATE OR REPLACE FUNCTION trg_contacts_updated() RETURNS TRIGGER AS $$
BEGIN NEW.updated_at = NOW(); RETURN NEW; END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_crm_contacts_updated ON crm_contacts;
CREATE TRIGGER trg_crm_contacts_updated
BEFORE UPDATE ON crm_contacts
FOR EACH ROW EXECUTE FUNCTION trg_contacts_updated();
-- 2. sales_logs 新增 contact_ids 字段 (JSONB Array,允许一次沟通涉及多个联系人)
ALTER TABLE sales_logs ADD COLUMN IF NOT EXISTS contact_ids JSONB DEFAULT '[]'::jsonb;
COMMENT ON COLUMN sales_logs.contact_ids IS '本次沟通涉及的联系人 UUID 列表 (JSONB Array)';
+539
View File
@@ -0,0 +1,539 @@
-- ============================================================
-- 润滑油行业 B2B ERP/CRM 综合系统 - PostgreSQL 数据库建模
-- 技术栈: Python (FastAPI) + PostgreSQL
-- 生成时间: 2026-02-27
-- ============================================================
-- 启用 uuid-ossp 扩展(用于 UUID 主键生成)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ============================================================
-- 通用函数:自动更新 updated_at 触发器
-- ============================================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- ************************************************************
-- 1. RBAC 权限域
-- ************************************************************
-- ============================================================
-- 1.1 部门树表 sys_departments
-- ============================================================
CREATE TABLE sys_departments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
parent_id UUID REFERENCES sys_departments(id),
name VARCHAR(100) NOT NULL,
sort_order INT DEFAULT 0,
status SMALLINT DEFAULT 1, -- 1:启用 0:停用
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE sys_departments IS '部门树表';
COMMENT ON COLUMN sys_departments.id IS '部门主键 UUID';
COMMENT ON COLUMN sys_departments.parent_id IS '父级部门IDNULL表示顶级';
COMMENT ON COLUMN sys_departments.name IS '部门名称';
COMMENT ON COLUMN sys_departments.sort_order IS '排序序号';
COMMENT ON COLUMN sys_departments.status IS '状态 1:启用 0:停用';
CREATE INDEX idx_dept_parent ON sys_departments(parent_id) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_dept_updated
BEFORE UPDATE ON sys_departments
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 1.2 角色表 sys_roles
-- ============================================================
CREATE TABLE sys_roles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
role_name VARCHAR(50) NOT NULL UNIQUE,
data_scope VARCHAR(20) NOT NULL DEFAULT 'self', -- all / dept_and_sub / self
menu_keys JSONB DEFAULT '[]'::JSONB, -- 菜单与按钮权限键集合
description VARCHAR(255),
status SMALLINT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE sys_roles IS '角色表';
COMMENT ON COLUMN sys_roles.id IS '角色主键 UUID';
COMMENT ON COLUMN sys_roles.role_name IS '角色名称';
COMMENT ON COLUMN sys_roles.data_scope IS '数据权限范围: all=全部 / dept_and_sub=本部门及下属 / self=仅本人';
COMMENT ON COLUMN sys_roles.menu_keys IS '拥有的菜单/按钮权限键列表 (JSONB数组)';
CREATE TRIGGER trg_role_updated
BEFORE UPDATE ON sys_roles
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 1.3 员工/账号表 sys_users
-- ============================================================
CREATE TABLE sys_users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
dept_id UUID REFERENCES sys_departments(id),
role_id UUID REFERENCES sys_roles(id),
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
real_name VARCHAR(50),
phone VARCHAR(20),
email VARCHAR(100),
avatar_url VARCHAR(500),
status SMALLINT DEFAULT 1, -- 1:在职 0:离职
last_login_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE sys_users IS '员工/账号表';
COMMENT ON COLUMN sys_users.id IS '用户主键 UUID';
COMMENT ON COLUMN sys_users.dept_id IS '所属部门ID';
COMMENT ON COLUMN sys_users.role_id IS '所属角色ID';
COMMENT ON COLUMN sys_users.username IS '登录账号';
COMMENT ON COLUMN sys_users.password_hash IS '密码哈希值';
COMMENT ON COLUMN sys_users.real_name IS '真实姓名';
COMMENT ON COLUMN sys_users.status IS '状态 1:在职 0:离职';
CREATE INDEX idx_user_dept ON sys_users(dept_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_user_role ON sys_users(role_id) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_user_updated
BEFORE UPDATE ON sys_users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ************************************************************
-- 2. CRM 客户域
-- ************************************************************
-- ============================================================
-- 2.1 客户主表 crm_customers
-- ============================================================
CREATE TABLE crm_customers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(200) NOT NULL,
level CHAR(1) DEFAULT 'C', -- A / B / C 三级
industry VARCHAR(100),
contact VARCHAR(50), -- 联系人姓名
phone VARCHAR(30),
email VARCHAR(100),
address TEXT,
ai_score NUMERIC(5,2) DEFAULT 0, -- AI 客情健康度评分 0~100
owner_id UUID REFERENCES sys_users(id), -- 负责销售
status SMALLINT DEFAULT 1, -- 1:活跃 0:冻结
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE crm_customers IS '客户主表';
COMMENT ON COLUMN crm_customers.id IS '客户主键 UUID';
COMMENT ON COLUMN crm_customers.name IS '客户/公司名称';
COMMENT ON COLUMN crm_customers.level IS '客户等级 A/B/C';
COMMENT ON COLUMN crm_customers.ai_score IS 'AI客情健康度评分 0~100';
COMMENT ON COLUMN crm_customers.owner_id IS '负责销售ID';
CREATE INDEX idx_cust_level ON crm_customers(level) WHERE is_deleted = FALSE;
CREATE INDEX idx_cust_owner ON crm_customers(owner_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_cust_name ON crm_customers(name) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_cust_updated
BEFORE UPDATE ON crm_customers
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 2.2 客户跟进日志表 crm_follow_up_logs
-- ============================================================
CREATE TABLE crm_follow_up_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
customer_id UUID NOT NULL REFERENCES crm_customers(id),
salesperson_id UUID NOT NULL REFERENCES sys_users(id),
content TEXT NOT NULL,
emotion_type VARCHAR(20), -- positive / neutral / negative
next_visit_time TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE crm_follow_up_logs IS '客户跟进日志表(AI简报数据源)';
COMMENT ON COLUMN crm_follow_up_logs.customer_id IS '关联客户ID';
COMMENT ON COLUMN crm_follow_up_logs.salesperson_id IS '操作销售人员ID';
COMMENT ON COLUMN crm_follow_up_logs.content IS '跟进内容';
COMMENT ON COLUMN crm_follow_up_logs.emotion_type IS '情感标记: positive/neutral/negative';
COMMENT ON COLUMN crm_follow_up_logs.next_visit_time IS '计划下次拜访时间';
CREATE INDEX idx_follow_cust ON crm_follow_up_logs(customer_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_follow_sales ON crm_follow_up_logs(salesperson_id) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_follow_updated
BEFORE UPDATE ON crm_follow_up_logs
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ************************************************************
-- 3. ERP 供应链域
-- ************************************************************
-- ============================================================
-- 3.1 产品分类树 erp_product_categories
-- ============================================================
CREATE TABLE erp_product_categories (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
parent_id UUID REFERENCES erp_product_categories(id),
name VARCHAR(100) NOT NULL,
sort_order INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE erp_product_categories IS '产品分类树表(左树结构支撑)';
COMMENT ON COLUMN erp_product_categories.parent_id IS '父级分类IDNULL为顶级分类';
CREATE INDEX idx_pcat_parent ON erp_product_categories(parent_id) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_pcat_updated
BEFORE UPDATE ON erp_product_categories
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 3.2 产品 SKU 表 erp_product_skus
-- ============================================================
CREATE TABLE erp_product_skus (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
category_id UUID REFERENCES erp_product_categories(id),
sku_code VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
spec VARCHAR(100), -- 规格,如 200L/桶
standard_price NUMERIC(12,2) NOT NULL DEFAULT 0,
stock_qty NUMERIC(12,2) NOT NULL DEFAULT 0,
warning_threshold NUMERIC(12,2) DEFAULT 0, -- 库存预警阈值
unit VARCHAR(20) DEFAULT '',
status SMALLINT DEFAULT 1, -- 1:在售 0:停售
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE erp_product_skus IS '产品SKU主档表';
COMMENT ON COLUMN erp_product_skus.sku_code IS 'SKU编号,全局唯一';
COMMENT ON COLUMN erp_product_skus.spec IS '包装规格,如 200L/桶、18L/桶';
COMMENT ON COLUMN erp_product_skus.standard_price IS '标准售价';
COMMENT ON COLUMN erp_product_skus.stock_qty IS '当前库存数量';
COMMENT ON COLUMN erp_product_skus.warning_threshold IS '库存预警阈值,低于此值触发预警';
CREATE INDEX idx_sku_category ON erp_product_skus(category_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_sku_code ON erp_product_skus(sku_code) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_sku_updated
BEFORE UPDATE ON erp_product_skus
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 3.3 出入库流水表 erp_inventory_flows
-- ============================================================
CREATE TABLE erp_inventory_flows (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
sku_id UUID NOT NULL REFERENCES erp_product_skus(id),
change_qty NUMERIC(12,2) NOT NULL, -- 正数=入库,负数=出库
reason VARCHAR(50) NOT NULL, -- purchase/shipment/loss/adjust
remark TEXT,
operator_id UUID REFERENCES sys_users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE erp_inventory_flows IS '出入库流水账表';
COMMENT ON COLUMN erp_inventory_flows.sku_id IS '关联SKU ID';
COMMENT ON COLUMN erp_inventory_flows.change_qty IS '变动数量:正=入库 负=出库';
COMMENT ON COLUMN erp_inventory_flows.reason IS '变动原因: purchase/shipment/loss/adjust';
COMMENT ON COLUMN erp_inventory_flows.operator_id IS '操作人ID';
CREATE INDEX idx_invflow_sku ON erp_inventory_flows(sku_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_invflow_operator ON erp_inventory_flows(operator_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_invflow_time ON erp_inventory_flows(created_at) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_invflow_updated
BEFORE UPDATE ON erp_inventory_flows
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ************************************************************
-- 4. ERP 交易域
-- ************************************************************
-- ============================================================
-- 4.1 订单主表 erp_orders
-- ============================================================
CREATE TABLE erp_orders (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_no VARCHAR(30) NOT NULL UNIQUE, -- 业务单号,如 ORD-20260227-001
customer_id UUID NOT NULL REFERENCES crm_customers(id),
salesperson_id UUID REFERENCES sys_users(id),
total_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
shipping_state VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending / partial / shipped
payment_state VARCHAR(20) NOT NULL DEFAULT 'unpaid', -- unpaid / partial / cleared
paid_amount NUMERIC(14,2) DEFAULT 0,
remark TEXT,
order_date DATE DEFAULT CURRENT_DATE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE erp_orders IS '订单主表';
COMMENT ON COLUMN erp_orders.order_no IS '业务订单号';
COMMENT ON COLUMN erp_orders.customer_id IS '下单客户ID';
COMMENT ON COLUMN erp_orders.salesperson_id IS '负责销售ID';
COMMENT ON COLUMN erp_orders.total_amount IS '订单总金额';
COMMENT ON COLUMN erp_orders.shipping_state IS '发货状态: pending/partial/shipped';
COMMENT ON COLUMN erp_orders.payment_state IS '付款状态: unpaid/partial/cleared';
COMMENT ON COLUMN erp_orders.paid_amount IS '已付金额';
CREATE INDEX idx_order_no ON erp_orders(order_no) WHERE is_deleted = FALSE;
CREATE INDEX idx_order_cust ON erp_orders(customer_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_order_sales ON erp_orders(salesperson_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_order_date ON erp_orders(order_date) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_order_updated
BEFORE UPDATE ON erp_orders
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 4.2 订单明细表 erp_order_items
-- ============================================================
CREATE TABLE erp_order_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_id UUID NOT NULL REFERENCES erp_orders(id),
sku_id UUID NOT NULL REFERENCES erp_product_skus(id),
qty NUMERIC(12,2) NOT NULL,
unit_price NUMERIC(12,2) NOT NULL, -- 本次成交单价(可能是专属价)
sub_total NUMERIC(14,2) NOT NULL,
shipped_qty NUMERIC(12,2) DEFAULT 0, -- 已发货累计量(用于分批发货扣减)
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE erp_order_items IS '订单明细行表';
COMMENT ON COLUMN erp_order_items.order_id IS '归属订单ID';
COMMENT ON COLUMN erp_order_items.sku_id IS '关联SKU ID';
COMMENT ON COLUMN erp_order_items.qty IS '下单数量';
COMMENT ON COLUMN erp_order_items.unit_price IS '成交单价(可能为客户专属价)';
COMMENT ON COLUMN erp_order_items.sub_total IS '小计金额 = qty * unit_price';
COMMENT ON COLUMN erp_order_items.shipped_qty IS '已发货累计数量,用于分批发货进度追踪';
CREATE INDEX idx_oitem_order ON erp_order_items(order_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_oitem_sku ON erp_order_items(sku_id) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_oitem_updated
BEFORE UPDATE ON erp_order_items
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ************************************************************
-- 5. ERP 物流域
-- ************************************************************
-- ============================================================
-- 5.1 发货主单表 erp_shipping_records
-- ============================================================
CREATE TABLE erp_shipping_records (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
shipping_no VARCHAR(30) NOT NULL UNIQUE, -- 发货单号 SHP-xxx
order_id UUID NOT NULL REFERENCES erp_orders(id),
carrier VARCHAR(100), -- 承运方,如德邦
tracking_no VARCHAR(100), -- 物流追踪号
status VARCHAR(20) NOT NULL DEFAULT 'transit', -- transit / delivered
ship_date DATE DEFAULT CURRENT_DATE,
remark TEXT,
operator_id UUID REFERENCES sys_users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE erp_shipping_records IS '发货主单表';
COMMENT ON COLUMN erp_shipping_records.shipping_no IS '发货单号';
COMMENT ON COLUMN erp_shipping_records.order_id IS '关联订单ID';
COMMENT ON COLUMN erp_shipping_records.carrier IS '承运方';
COMMENT ON COLUMN erp_shipping_records.tracking_no IS '物流追踪号';
COMMENT ON COLUMN erp_shipping_records.status IS '物流状态: transit=运输中 / delivered=已签收';
CREATE INDEX idx_ship_order ON erp_shipping_records(order_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_ship_no ON erp_shipping_records(shipping_no) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_ship_updated
BEFORE UPDATE ON erp_shipping_records
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 5.2 发货明细表 erp_shipping_items
-- ============================================================
CREATE TABLE erp_shipping_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
shipping_id UUID NOT NULL REFERENCES erp_shipping_records(id),
order_item_id UUID NOT NULL REFERENCES erp_order_items(id),
sku_id UUID NOT NULL REFERENCES erp_product_skus(id),
shipped_qty NUMERIC(12,2) NOT NULL, -- 本次发货数量
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE erp_shipping_items IS '发货明细行表(承接分批发货扣减)';
COMMENT ON COLUMN erp_shipping_items.shipping_id IS '归属发货单ID';
COMMENT ON COLUMN erp_shipping_items.order_item_id IS '关联订单明细行ID(用于回写已发货量)';
COMMENT ON COLUMN erp_shipping_items.sku_id IS '关联SKU ID';
COMMENT ON COLUMN erp_shipping_items.shipped_qty IS '本次实际发货数量';
CREATE INDEX idx_sitem_shipping ON erp_shipping_items(shipping_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_sitem_oitem ON erp_shipping_items(order_item_id) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_sitem_updated
BEFORE UPDATE ON erp_shipping_items
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ************************************************************
-- 6. 财务票据域
-- ************************************************************
-- ============================================================
-- 6.1 统一发票池表 fin_invoice_pool
-- ============================================================
CREATE TABLE fin_invoice_pool (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
uploader_id UUID REFERENCES sys_users(id),
file_url VARCHAR(500),
merchant_name VARCHAR(200),
amount NUMERIC(14,2) NOT NULL DEFAULT 0,
invoice_date DATE,
type VARCHAR(30) NOT NULL DEFAULT 'expense', -- customer=客户发票 / expense=报销发票
ai_extracted_data JSONB DEFAULT '{}'::JSONB, -- OCR + AI 解析的原始结构化数据
is_used BOOLEAN DEFAULT FALSE, -- 是否已被报销单引用
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE -- 软删除=作废
);
COMMENT ON TABLE fin_invoice_pool IS '统一发票池表(上传即OCR提取)';
COMMENT ON COLUMN fin_invoice_pool.uploader_id IS '上传人ID';
COMMENT ON COLUMN fin_invoice_pool.file_url IS '原始票据文件URL';
COMMENT ON COLUMN fin_invoice_pool.merchant_name IS '商户/开票方名称';
COMMENT ON COLUMN fin_invoice_pool.amount IS '票面金额';
COMMENT ON COLUMN fin_invoice_pool.type IS '票据类型: customer=客户发票 / expense=报销发票';
COMMENT ON COLUMN fin_invoice_pool.ai_extracted_data IS 'AI/OCR提取的结构化数据 (JSONB)';
COMMENT ON COLUMN fin_invoice_pool.is_used IS '是否已被报销单引用';
COMMENT ON COLUMN fin_invoice_pool.is_deleted IS '软删除标识(作废,防物理篡改)';
CREATE INDEX idx_inv_uploader ON fin_invoice_pool(uploader_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_inv_type ON fin_invoice_pool(type) WHERE is_deleted = FALSE;
CREATE INDEX idx_inv_date ON fin_invoice_pool(invoice_date) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_inv_updated
BEFORE UPDATE ON fin_invoice_pool
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 6.2 报销单主表 fin_expense_records
-- ============================================================
CREATE TABLE fin_expense_records (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
system_no VARCHAR(30) NOT NULL UNIQUE, -- 系统单号 EXP-xxx
applicant_id UUID NOT NULL REFERENCES sys_users(id),
total_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- draft/pending/approved/rejected
remark TEXT,
approved_by UUID REFERENCES sys_users(id),
approved_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE fin_expense_records IS '报销单主表';
COMMENT ON COLUMN fin_expense_records.system_no IS '系统报销单号';
COMMENT ON COLUMN fin_expense_records.applicant_id IS '申请人ID';
COMMENT ON COLUMN fin_expense_records.total_amount IS '报销总金额';
COMMENT ON COLUMN fin_expense_records.status IS '报销状态: draft/pending/approved/rejected';
COMMENT ON COLUMN fin_expense_records.approved_by IS '审批人ID';
CREATE INDEX idx_exp_applicant ON fin_expense_records(applicant_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_exp_status ON fin_expense_records(status) WHERE is_deleted = FALSE;
CREATE INDEX idx_exp_no ON fin_expense_records(system_no) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_exp_updated
BEFORE UPDATE ON fin_expense_records
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 6.3 报销明细行表 fin_expense_details
-- ============================================================
CREATE TABLE fin_expense_details (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
expense_id UUID NOT NULL REFERENCES fin_expense_records(id),
invoice_id UUID REFERENCES fin_invoice_pool(id),
expense_desc VARCHAR(500), -- 费用说明
original_type VARCHAR(50), -- 原始费用类型: fuel/entertainment/travel/office
offset_type VARCHAR(50), -- 冲顶类型(二级类型转换后)
amount NUMERIC(14,2) NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE
);
COMMENT ON TABLE fin_expense_details IS '报销明细行表';
COMMENT ON COLUMN fin_expense_details.expense_id IS '归属报销单ID';
COMMENT ON COLUMN fin_expense_details.invoice_id IS '关联发票ID';
COMMENT ON COLUMN fin_expense_details.expense_desc IS '费用说明描述';
COMMENT ON COLUMN fin_expense_details.original_type IS '原始费用类型: fuel/entertainment/travel/office';
COMMENT ON COLUMN fin_expense_details.offset_type IS '冲顶类型(二级票据类型转换后的类型)';
COMMENT ON COLUMN fin_expense_details.amount IS '本行金额';
CREATE INDEX idx_expd_expense ON fin_expense_details(expense_id) WHERE is_deleted = FALSE;
CREATE INDEX idx_expd_invoice ON fin_expense_details(invoice_id) WHERE is_deleted = FALSE;
CREATE TRIGGER trg_expd_updated
BEFORE UPDATE ON fin_expense_details
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- 完成:输出建表结果摘要
-- ============================================================
-- 共创建 13 张业务表:
-- RBAC 权限域: sys_departments, sys_roles, sys_users
-- CRM 客户域: crm_customers, crm_follow_up_logs
-- ERP 供应链域: erp_product_categories, erp_product_skus, erp_inventory_flows
-- ERP 交易域: erp_orders, erp_order_items
-- ERP 物流域: erp_shipping_records, erp_shipping_items
-- 财务票据域: fin_invoice_pool, fin_expense_records, fin_expense_details
--
-- 全局规范落地:
-- ✓ 主键统一 UUID (uuid_generate_v4)
-- ✓ 每表必带 created_at / updated_at / is_deleted 审计三件套
-- ✓ updated_at 自动触发器 (update_updated_at_column)
-- ✓ 灵活字段采用 JSONB (menu_keys, ai_extracted_data)
-- ✓ 外键关联完整,部分索引 (WHERE is_deleted=FALSE) 优化查询
+41
View File
@@ -0,0 +1,41 @@
-- ============================================================
-- 种子数据:用于测试登录的初始管理员账号
-- 密码: 123456 (bcrypt hash)
-- 执行方式: psql -U postgres -d crm_erp -f seed.sql
-- ============================================================
-- 1. 插入顶级部门
INSERT INTO sys_departments (id, parent_id, name, sort_order, status)
VALUES (
'a0000000-0000-0000-0000-000000000001',
NULL,
'总部',
1,
1
) ON CONFLICT DO NOTHING;
-- 2. 插入超级管理员角色 (data_scope = all)
INSERT INTO sys_roles (id, role_name, data_scope, menu_keys, description, status)
VALUES (
'b0000000-0000-0000-0000-000000000001',
'超级管理员',
'all',
'["dashboard","customers","customers:detail","products","orders","shipping","finance","settings"]'::JSONB,
'拥有系统全部权限',
1
) ON CONFLICT DO NOTHING;
-- 3. 插入管理员用户 (admin / 123456)
-- bcrypt hash of "123456" —— 直接用 Python 生成:
-- from passlib.context import CryptContext; print(CryptContext(schemes=["bcrypt"]).hash("123456"))
INSERT INTO sys_users (id, dept_id, role_id, username, password_hash, real_name, phone, status)
VALUES (
'c0000000-0000-0000-0000-000000000001',
'a0000000-0000-0000-0000-000000000001',
'b0000000-0000-0000-0000-000000000001',
'admin',
'$2b$12$N3aYJxXxEeUggclCvnVLgewUFIewfYhAB2fXLlWzI8lY4RoPjCbia',
'系统管理员',
'13800000000',
1
) ON CONFLICT DO NOTHING;