-- 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,