Files
yunhaifinance/temp-clone/company_finance_db_schema.sql
T

448 lines
16 KiB
PL/PgSQL

-- Company Finance Database Schema
-- PostgreSQL 15+
-- Created: 2026-03-08
-- Drop existing database if exists and create new one
DROP DATABASE IF EXISTS company_finance_db;
CREATE DATABASE company_finance_db
WITH
OWNER = postgres
ENCODING = 'UTF8'
LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.UTF-8'
TABLESPACE = pg_default
CONNECTION LIMIT = -1
IS_TEMPLATE = False;
-- Connect to the new database
\c company_finance_db;
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ============================================
-- 1. Users Table (假设已存在,这里创建简化版本)
-- ============================================
CREATE TABLE IF NOT EXISTS users (
user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
full_name_zh VARCHAR(100),
full_name_th VARCHAR(100),
full_name_en VARCHAR(100),
role VARCHAR(50) NOT NULL DEFAULT 'user',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- 2. Product Categories Table (商品分类表)
-- ============================================
CREATE TABLE product_categories (
category_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
category_code VARCHAR(20) UNIQUE NOT NULL,
name_zh VARCHAR(100) NOT NULL,
name_th VARCHAR(100),
name_en VARCHAR(100),
description_zh TEXT,
description_th TEXT,
description_en TEXT,
parent_category_id UUID REFERENCES product_categories(category_id) ON DELETE SET NULL,
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_by UUID REFERENCES users(user_id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- 3. Products Table (商品表)
-- ============================================
CREATE TABLE products (
product_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
product_code VARCHAR(50) UNIQUE NOT NULL,
sku VARCHAR(50) UNIQUE,
name_zh VARCHAR(200) NOT NULL,
name_th VARCHAR(200),
name_en VARCHAR(200),
description_zh TEXT,
description_th TEXT,
description_en TEXT,
category_id UUID REFERENCES product_categories(category_id) ON DELETE SET NULL,
unit_price NUMERIC(15,2) NOT NULL DEFAULT 0.00,
currency VARCHAR(3) DEFAULT 'CNY',
unit_type VARCHAR(50) DEFAULT 'piece',
specifications JSONB,
is_active BOOLEAN DEFAULT TRUE,
created_by UUID REFERENCES users(user_id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- 4. Projects Table (项目表)
-- ============================================
CREATE TABLE projects (
project_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
project_code VARCHAR(50) UNIQUE NOT NULL,
name_zh VARCHAR(200) NOT NULL,
name_th VARCHAR(200),
name_en VARCHAR(200),
description_zh TEXT,
description_th TEXT,
description_en TEXT,
client_name_zh VARCHAR(200),
client_name_th VARCHAR(200),
client_name_en VARCHAR(200),
contract_number VARCHAR(100),
contract_amount NUMERIC(15,2) NOT NULL DEFAULT 0.00,
currency VARCHAR(3) DEFAULT 'CNY',
start_date DATE,
end_date DATE,
status VARCHAR(50) DEFAULT 'planning' CHECK (status IN ('planning', 'in_progress', 'completed', 'cancelled', 'on_hold')),
project_manager_id UUID REFERENCES users(user_id) ON DELETE SET NULL,
is_active BOOLEAN DEFAULT TRUE,
created_by UUID REFERENCES users(user_id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- 5. Payment Nodes Table (付款节点表)
-- ============================================
CREATE TABLE payment_nodes (
node_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
project_id UUID REFERENCES projects(project_id) ON DELETE CASCADE,
node_code VARCHAR(50) NOT NULL,
name_zh VARCHAR(200) NOT NULL,
name_th VARCHAR(200),
name_en VARCHAR(200),
description_zh TEXT,
description_th TEXT,
description_en TEXT,
planned_date DATE NOT NULL,
actual_date DATE,
planned_amount NUMERIC(15,2) NOT NULL DEFAULT 0.00,
actual_amount NUMERIC(15,2),
currency VARCHAR(3) DEFAULT 'CNY',
node_type VARCHAR(50) DEFAULT 'payment' CHECK (node_type IN ('payment', 'receipt', 'milestone')),
status VARCHAR(50) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'completed', 'cancelled', 'delayed')),
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_by UUID REFERENCES users(user_id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(project_id, node_code)
);
-- ============================================
-- 6. Payment Records Table (收付款记录表)
-- ============================================
CREATE TABLE payment_records (
record_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
node_id UUID REFERENCES payment_nodes(node_id) ON DELETE CASCADE,
record_type VARCHAR(50) NOT NULL CHECK (record_type IN ('payment', 'receipt')),
amount NUMERIC(15,2) NOT NULL DEFAULT 0.00,
currency VARCHAR(3) DEFAULT 'CNY',
exchange_rate NUMERIC(10,6) DEFAULT 1.000000,
converted_amount NUMERIC(15,2),
payment_date DATE NOT NULL,
payment_method VARCHAR(50) DEFAULT 'bank_transfer' CHECK (payment_method IN ('bank_transfer', 'cash', 'check', 'credit_card', 'digital_wallet')),
reference_number VARCHAR(100),
bank_name VARCHAR(200),
account_number VARCHAR(100),
payer_name VARCHAR(200),
payee_name VARCHAR(200),
description TEXT,
status VARCHAR(50) DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed', 'cancelled')),
attachment_urls TEXT[],
verified_by UUID REFERENCES users(user_id) ON DELETE SET NULL,
verified_at TIMESTAMP WITH TIME ZONE,
is_active BOOLEAN DEFAULT TRUE,
created_by UUID REFERENCES users(user_id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- 7. Exchange Rates Table (汇率表)
-- ============================================
CREATE TABLE exchange_rates (
rate_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
base_currency VARCHAR(3) NOT NULL,
target_currency VARCHAR(3) NOT NULL,
exchange_rate NUMERIC(10,6) NOT NULL,
effective_date DATE NOT NULL,
source VARCHAR(100) DEFAULT 'manual',
is_active BOOLEAN DEFAULT TRUE,
created_by UUID REFERENCES users(user_id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(base_currency, target_currency, effective_date)
);
-- ============================================
-- 8. Language Configs Table (多语言配置表)
-- ============================================
CREATE TABLE language_configs (
config_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
config_key VARCHAR(100) NOT NULL,
module VARCHAR(50) NOT NULL,
value_zh TEXT NOT NULL,
value_th TEXT,
value_en TEXT,
description TEXT,
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_by UUID REFERENCES users(user_id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(config_key, module)
);
-- ============================================
-- 9. Project Products Table (项目商品关联表)
-- ============================================
CREATE TABLE project_products (
project_product_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
project_id UUID REFERENCES projects(project_id) ON DELETE CASCADE,
product_id UUID REFERENCES products(product_id) ON DELETE CASCADE,
quantity INTEGER NOT NULL DEFAULT 1,
unit_price NUMERIC(15,2) NOT NULL DEFAULT 0.00,
currency VARCHAR(3) DEFAULT 'CNY',
total_amount NUMERIC(15,2) GENERATED ALWAYS AS (quantity * unit_price) STORED,
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_by UUID REFERENCES users(user_id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(project_id, product_id)
);
-- ============================================
-- INDEXES
-- ============================================
-- Users indexes
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
-- Product categories indexes
CREATE INDEX idx_product_categories_code ON product_categories(category_code);
CREATE INDEX idx_product_categories_parent ON product_categories(parent_category_id);
CREATE INDEX idx_product_categories_active ON product_categories(is_active);
-- Products indexes
CREATE INDEX idx_products_code ON products(product_code);
CREATE INDEX idx_products_sku ON products(sku);
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_active ON products(is_active);
CREATE INDEX idx_products_price ON products(unit_price);
-- Projects indexes
CREATE INDEX idx_projects_code ON projects(project_code);
CREATE INDEX idx_projects_status ON projects(status);
CREATE INDEX idx_projects_manager ON projects(project_manager_id);
CREATE INDEX idx_projects_dates ON projects(start_date, end_date);
CREATE INDEX idx_projects_active ON projects(is_active);
-- Payment nodes indexes
CREATE INDEX idx_payment_nodes_project ON payment_nodes(project_id);
CREATE INDEX idx_payment_nodes_code ON payment_nodes(node_code);
CREATE INDEX idx_payment_nodes_dates ON payment_nodes(planned_date, actual_date);
CREATE INDEX idx_payment_nodes_status ON payment_nodes(status);
CREATE INDEX idx_payment_nodes_type ON payment_nodes(node_type);
-- Payment records indexes
CREATE INDEX idx_payment_records_node ON payment_records(node_id);
CREATE INDEX idx_payment_records_type ON payment_records(record_type);
CREATE INDEX idx_payment_records_date ON payment_records(payment_date);
CREATE INDEX idx_payment_records_status ON payment_records(status);
CREATE INDEX idx_payment_records_method ON payment_records(payment_method);
-- Exchange rates indexes
CREATE INDEX idx_exchange_rates_currencies ON exchange_rates(base_currency, target_currency);
CREATE INDEX idx_exchange_rates_date ON exchange_rates(effective_date);
CREATE INDEX idx_exchange_rates_active ON exchange_rates(is_active);
-- Language configs indexes
CREATE INDEX idx_language_configs_key ON language_configs(config_key);
CREATE INDEX idx_language_configs_module ON language_configs(module);
CREATE INDEX idx_language_configs_active ON language_configs(is_active);
-- Project products indexes
CREATE INDEX idx_project_products_project ON project_products(project_id);
CREATE INDEX idx_project_products_product ON project_products(product_id);
CREATE INDEX idx_project_products_active ON project_products(is_active);
-- ============================================
-- VIEWS
-- ============================================
-- View: 项目概览视图
CREATE OR REPLACE VIEW project_overview AS
SELECT
p.project_id,
p.project_code,
p.name_zh as project_name_zh,
p.name_en as project_name_en,
p.contract_amount,
p.currency,
p.start_date,
p.end_date,
p.status as project_status,
u.full_name_zh as project_manager_name,
COUNT(DISTINCT pp.product_id) as product_count,
COUNT(DISTINCT pn.node_id) as payment_node_count,
COALESCE(SUM(pr.amount), 0) as total_payments,
COALESCE(SUM(CASE WHEN pr.record_type = 'receipt' THEN pr.amount ELSE 0 END), 0) as total_receipts,
COALESCE(SUM(CASE WHEN pr.record_type = 'payment' THEN pr.amount ELSE 0 END), 0) as total_expenses
FROM projects p
LEFT JOIN users u ON p.project_manager_id = u.user_id
LEFT JOIN project_products pp ON p.project_id = pp.project_id
LEFT JOIN payment_nodes pn ON p.project_id = pn.project_id
LEFT JOIN payment_records pr ON pn.node_id = pr.node_id
WHERE p.is_active = TRUE
GROUP BY p.project_id, p.project_code, p.name_zh, p.name_en, p.contract_amount, p.currency,
p.start_date, p.end_date, p.status, u.full_name_zh;
-- View: 付款节点详情视图
CREATE OR REPLACE VIEW payment_node_details AS
SELECT
pn.node_id,
pn.project_id,
p.project_code,
p.name_zh as project_name_zh,
pn.node_code,
pn.name_zh as node_name_zh,
pn.planned_date,
pn.actual_date,
pn.planned_amount,
pn.actual_amount,
pn.currency,
pn.node_type,
pn.status as node_status,
COUNT(pr.record_id) as record_count,
COALESCE(SUM(pr.amount), 0) as total_recorded_amount,
CASE
WHEN pn.node_type = 'payment' THEN '支出'
WHEN pn.node_type = 'receipt' THEN '收入'
ELSE '里程碑'
END as node_type_cn
FROM payment_nodes pn
JOIN projects p ON pn.project_id = p.project_id
LEFT JOIN payment_records pr ON pn.node_id = pr.node_id AND pr.is_active = TRUE
WHERE pn.is_active = TRUE
GROUP BY pn.node_id, pn.project_id, p.project_code, p.name_zh, pn.node_code, pn.name_zh,
pn.planned_date, pn.actual_date, pn.planned_amount, pn.actual_amount, pn.currency,
pn.node_type, pn.status;
-- View: 商品分类树视图
CREATE OR REPLACE VIEW product_category_tree AS
WITH RECURSIVE category_tree AS (
SELECT
category_id,
category_code,
name_zh,
name_en,
parent_category_id,
1 as level,
ARRAY[category_code] as path_codes,
ARRAY[name_zh] as path_names
FROM product_categories
WHERE parent_category_id IS NULL AND is_active = TRUE
UNION ALL
SELECT
c.category_id,
c.category_code,
c.name_zh,
c.name_en,
c.parent_category_id,
ct.level + 1,
ct.path_codes || c.category_code,
ct.path_names || c.name_zh
FROM product_categories c
JOIN category_tree ct ON c.parent_category_id = ct.category_id
WHERE c.is_active = TRUE
)
SELECT
category_id,
category_code,
name_zh,
name_en,
parent_category_id,
level,
array_to_string(path_codes, ' > ') as full_path_code,
array_to_string(path_names, ' > ') as full_path_name
FROM category_tree
ORDER BY path_codes;
-- View: 汇率最新视图
CREATE OR REPLACE VIEW latest_exchange_rates AS
SELECT DISTINCT ON (base_currency, target_currency)
rate_id,
base_currency,
target_currency,
exchange_rate,
effective_date,
source,
created_at
FROM exchange_rates
WHERE is_active = TRUE
ORDER BY base_currency, target_currency, effective_date DESC;
-- ============================================
-- FUNCTIONS AND TRIGGERS
-- ============================================
-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Create triggers for all tables with updated_at column
DO $$
DECLARE
table_name text;
BEGIN
FOR table_name IN
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
AND tablename IN (
'users', 'product_categories', 'products', 'projects',
'payment_nodes', 'payment_records', 'exchange_rates',
'language_configs', 'project_products'
)
LOOP
EXECUTE format('
DROP TRIGGER IF EXISTS update_%s_updated_at ON %s;
CREATE TRIGGER update_%s_updated_at
BEFORE UPDATE ON %s
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
', table_name, table_name, table_name, table_name);
END LOOP;
END $$;
-- Function to calculate project financial summary
CREATE OR REPLACE FUNCTION calculate_project_financial_summary(project_uuid UUID)
RETURNS TABLE(
total_contract_amount NUMERIC,
total_planned_payments NUMERIC,
total_actual_payments NUMERIC,
total_planned_receipts NUMERIC,
total_actual_receipts NUMERIC,
balance NUMERIC
) AS $$
BEGIN
RETURN QUERY
SELECT
COALESCE(p.contract_amount,