Files
accounting-system/DATABASE_SCHEMA.md
Invoice Master 05ea67144f feat: initial project setup
- Add .NET 8 backend with Clean Architecture
- Add React + Vite + TypeScript frontend
- Implement authentication with JWT
- Implement Azure Blob Storage client
- Implement OCR integration
- Implement supplier matching service
- Implement voucher generation
- Implement Fortnox provider
- Add unit and integration tests
- Add Docker Compose configuration
2026-02-04 20:14:34 +01:00

28 KiB
Raw Blame History

Invoice Master - 数据库 Schema 设计

版本: v3.0
数据库: PostgreSQL 15+
ORM: Entity Framework Core 8
日期: 2026-02-03


1. ER 图 (多会计系统版)

┌──────────────────┐       ┌──────────────────────────┐       ┌──────────────────┐
│      users       │       │ accounting_connections   │       │ supplier_cache   │
├──────────────────┤       ├──────────────────────────┤       ├──────────────────┤
│ id (PK)          │◄──────┤ id (PK)                  │◄──────┤ id (PK)          │
│ email            │       │ user_id (FK)             │       │ connection_id(FK)│
│ hashed_password  │       │ provider (e.g. 'fortnox')│       │ supplier_number  │
│ is_active        │       │ access_token_encrypted   │       │ name             │
│ created_at       │       │ refresh_token_encrypted  │       │ org_number       │
└──────────────────┘       │ company_name             │       │ cached_at        │
                           │ company_org_number       │       └──────────────────┘
                           │ UNIQUE(user_id, provider)│
                           └───────────┬──────────────┘
                                       │
                                       │
                           ┌───────────▼───────────┐
                           │       invoices        │
                           ├───────────────────────┤
                           │ id (PK)               │
                           │ connection_id (FK)    │◄──────────────────────┐
                           │ provider              │                       │
                           │ file_path             │                       │
                           │ extraction_data       │                       │
                           │ supplier_number       │                       │
                           │ voucher_number        │                       │
                           │ status                │                       │
                           └───────────┬───────────┘                       │
                                       │                                   │
                                       │                                   │
                           ┌───────────▼───────────┐       ┌───────────────▼──┐
                           │   invoice_reviews     │       │ processing_queue │
                           ├───────────────────────┤       ├──────────────────┤
                           │ id (PK)               │       │ id (PK)          │
                           │ invoice_id (FK)       │       │ invoice_id (FK)  │
                           │ reviewed_by           │       │ status           │
                           │ corrections           │       │ retry_count      │
                           │ approved              │       │ error_message    │
                           └───────────────────────┘       └──────────────────┘

2. 表结构定义

2.1 users - 用户表

存储系统用户基本信息。

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email VARCHAR(255) UNIQUE NOT NULL,
    hashed_password VARCHAR(255) NOT NULL,
    full_name VARCHAR(255),
    is_active BOOLEAN DEFAULT true,
    is_superuser BOOLEAN DEFAULT false,
    
    -- 时间戳
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    last_login_at TIMESTAMP WITH TIME ZONE
);

-- 索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_active ON users(is_active);

2.2 accounting_connections - 会计系统连接表

存储多个会计系统的 OAuth 连接信息,一个用户可以连接多个会计系统。

CREATE TABLE accounting_connections (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    
    -- 会计系统标识
    provider VARCHAR(50) NOT NULL,  -- 'fortnox', 'visma', 'hogia', etc.
    
    -- OAuth Tokens (加密存储)
    access_token_encrypted TEXT NOT NULL,
    refresh_token_encrypted TEXT NOT NULL,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    scope TEXT,  -- 权限范围,不同 provider 格式不同
    
    -- 公司信息
    company_name VARCHAR(255),
    company_org_number VARCHAR(20),
    
    -- 默认设置 (各 provider 可能有不同的默认值)
    default_voucher_series VARCHAR(10) DEFAULT 'A',
    default_account_code INTEGER DEFAULT 5460,
    auto_attach_pdf BOOLEAN DEFAULT true,
    auto_create_supplier BOOLEAN DEFAULT false,
    
    -- 状态
    is_active BOOLEAN DEFAULT true,
    last_sync_at TIMESTAMP WITH TIME ZONE,
    
    -- 时间戳
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    -- 一个用户可连接多个会计系统,但不能重复连接同一个
    UNIQUE(user_id, provider)
);

-- 索引
CREATE INDEX idx_connections_user ON accounting_connections(user_id);
CREATE INDEX idx_connections_provider ON accounting_connections(provider);
CREATE INDEX idx_connections_active ON accounting_connections(is_active);
CREATE INDEX idx_connections_expires ON accounting_connections(expires_at);

2.3 invoices - 发票处理记录表

存储发票处理历史和状态,支持多会计系统。

CREATE TYPE invoice_status AS ENUM (
    'pending',      -- 等待处理
    'uploading',    -- 正在上传
    'processing',   -- OCR 处理中
    'preview',      -- 等待用户确认
    'importing',    -- 正在导入会计系统
    'imported',     -- 已成功导入
    'failed'        -- 处理失败
);

CREATE TYPE supplier_match_action AS ENUM (
    'USE_EXISTING',   -- 使用现有供应商
    'CREATE_NEW',     -- 创建新供应商
    'SUGGEST_MATCH'   -- 建议匹配
);

CREATE TABLE invoices (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    connection_id UUID NOT NULL REFERENCES accounting_connections(id) ON DELETE CASCADE,
    
    -- 冗余存储 provider便于查询
    provider VARCHAR(50) NOT NULL,
    
    -- 文件信息
    original_filename VARCHAR(255) NOT NULL,
    storage_path TEXT NOT NULL,
    file_size INTEGER,
    file_hash VARCHAR(64),  -- SHA-256 用于去重
    
    -- OCR 提取结果 (JSONB 存储结构化数据)
    extraction_data JSONB,
    extraction_confidence DECIMAL(3,2),  -- 0.00 - 1.00
    
    -- 提取的字段 (冗余存储便于查询)
    extracted_supplier_name VARCHAR(255),
    extracted_supplier_org_number VARCHAR(20),
    extracted_invoice_number VARCHAR(100),
    extracted_invoice_date DATE,
    extracted_due_date DATE,
    extracted_amount_total DECIMAL(12,2),
    extracted_amount_vat DECIMAL(12,2),
    extracted_vat_rate INTEGER,
    extracted_ocr_number VARCHAR(50),
    extracted_bankgiro VARCHAR(50),
    extracted_plusgiro VARCHAR(50),
    extracted_currency VARCHAR(3) DEFAULT 'SEK',
    
    -- 供应商匹配结果
    supplier_number VARCHAR(50),
    supplier_match_confidence DECIMAL(3,2),
    supplier_match_action supplier_match_action,
    
    -- 会计凭证信息
    voucher_series VARCHAR(10),
    voucher_number VARCHAR(50),
    voucher_url TEXT,
    
    -- 会计科目分配 (JSONB 数组)
    voucher_rows JSONB,
    
    -- 处理状态
    status invoice_status DEFAULT 'pending',
    error_message TEXT,
    error_code VARCHAR(50),
    
    -- 用户审核
    reviewed_by UUID REFERENCES users(id),
    reviewed_at TIMESTAMP WITH TIME ZONE,
    
    -- 附件信息
    attachment_id VARCHAR(100),
    attachment_url TEXT,
    
    -- 时间戳
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    processed_at TIMESTAMP WITH TIME ZONE
);

-- 索引
CREATE INDEX idx_invoices_connection ON invoices(connection_id);
CREATE INDEX idx_invoices_provider ON invoices(provider);
CREATE INDEX idx_invoices_status ON invoices(status);
CREATE INDEX idx_invoices_created ON invoices(created_at DESC);
CREATE INDEX idx_invoices_file_hash ON invoices(file_hash);
CREATE INDEX idx_invoices_supplier ON invoices(extracted_supplier_org_number);

-- GIN 索引用于 JSONB 查询
CREATE INDEX idx_invoices_extraction ON invoices USING GIN (extraction_data);

2.4 supplier_cache - 供应商缓存表

缓存各会计系统的供应商列表,减少 API 调用。

CREATE TABLE supplier_cache (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    connection_id UUID NOT NULL REFERENCES accounting_connections(id) ON DELETE CASCADE,
    
    -- 供应商信息 (各 provider 字段可能不同)
    supplier_number VARCHAR(50) NOT NULL,
    name VARCHAR(255) NOT NULL,
    organisation_number VARCHAR(20),
    address1 VARCHAR(255),
    address2 VARCHAR(255),
    postcode VARCHAR(20),
    city VARCHAR(100),
    country VARCHAR(100),
    phone VARCHAR(50),
    email VARCHAR(255),
    bankgiro_number VARCHAR(50),
    plusgiro_number VARCHAR(50),
    
    -- 缓存元数据
    cached_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    expires_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + INTERVAL '1 hour',
    
    UNIQUE(connection_id, supplier_number)
);

-- 索引
CREATE INDEX idx_supplier_cache_connection ON supplier_cache(connection_id);
CREATE INDEX idx_supplier_cache_org ON supplier_cache(organisation_number);
CREATE INDEX idx_supplier_cache_name ON supplier_cache(name);
CREATE INDEX idx_supplier_cache_expires ON supplier_cache(expires_at);

2.5 invoice_reviews - 发票审核记录表

存储用户对 OCR 结果的修改和确认。

CREATE TABLE invoice_reviews (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
    
    -- 审核人
    reviewed_by UUID NOT NULL REFERENCES users(id),
    
    -- 用户修正的数据 (JSONB)
    corrections JSONB,
    
    -- 审核结果
    approved BOOLEAN NOT NULL,
    rejection_reason TEXT,
    
    -- 时间戳
    reviewed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 索引
CREATE INDEX idx_reviews_invoice ON invoice_reviews(invoice_id);
CREATE INDEX idx_reviews_user ON invoice_reviews(reviewed_by);

2.6 processing_queue - 处理队列表

异步任务队列,用于发票处理。

CREATE TYPE queue_status AS ENUM (
    'queued',
    'processing',
    'completed',
    'failed',
    'cancelled'
);

CREATE TABLE processing_queue (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
    
    -- 任务类型
    task_type VARCHAR(50) NOT NULL,  -- 'ocr', 'import', 'upload_attachment'
    
    -- 状态
    status queue_status DEFAULT 'queued',
    priority INTEGER DEFAULT 5,  -- 1-10, 数字越小优先级越高
    
    -- 重试机制
    retry_count INTEGER DEFAULT 0,
    max_retries INTEGER DEFAULT 3,
    
    -- 调度
    scheduled_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    started_at TIMESTAMP WITH TIME ZONE,
    completed_at TIMESTAMP WITH TIME ZONE,
    
    -- 错误信息
    error_message TEXT,
    error_stack TEXT,
    
    -- 处理结果
    result_data JSONB,
    
    -- 工作节点
    worker_id VARCHAR(100)
);

-- 索引
CREATE INDEX idx_queue_status ON processing_queue(status);
CREATE INDEX idx_queue_scheduled ON processing_queue(scheduled_at);
CREATE INDEX idx_queue_invoice ON processing_queue(invoice_id);
CREATE INDEX idx_queue_priority ON processing_queue(status, priority, scheduled_at);

2.7 account_mappings - 科目映射表

存储用户自定义的会计科目映射规则,支持多会计系统。

CREATE TABLE account_mappings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    connection_id UUID NOT NULL REFERENCES accounting_connections(id) ON DELETE CASCADE,
    
    -- 会计系统标识
    provider VARCHAR(50) NOT NULL,
    
    -- 匹配规则
    supplier_org_number VARCHAR(20),  -- 特定供应商
    keyword VARCHAR(100),             -- 关键词匹配
    
    -- 科目设置 (不同 provider 可能有不同的科目体系)
    account_code INTEGER NOT NULL,
    account_name VARCHAR(255),
    vat_rate INTEGER DEFAULT 25,
    
    -- 描述模板
    description_template VARCHAR(255),  -- e.g., "{supplier_name} - {invoice_number}"
    
    -- 优先级 (数字越大优先级越高)
    priority INTEGER DEFAULT 0,
    
    -- 状态
    is_active BOOLEAN DEFAULT true,
    
    -- 时间戳
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    -- 约束: supplier_org_number 和 keyword 至少有一个
    CONSTRAINT check_mapping_rule CHECK (
        supplier_org_number IS NOT NULL OR keyword IS NOT NULL
    )
);

-- 索引
CREATE INDEX idx_mappings_connection ON account_mappings(connection_id);
CREATE INDEX idx_mappings_provider ON account_mappings(provider);
CREATE INDEX idx_mappings_supplier ON account_mappings(supplier_org_number);
CREATE INDEX idx_mappings_keyword ON account_mappings(keyword);

2.8 api_logs - API 日志表

记录关键 API 调用用于审计和调试。

CREATE TABLE api_logs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
    -- 请求信息
    request_id VARCHAR(100) NOT NULL,
    connection_id UUID REFERENCES accounting_connections(id),
    user_id UUID REFERENCES users(id),
    
    -- API 详情
    provider VARCHAR(50),  -- 'fortnox', 'visma', 'ocr', 'internal'
    endpoint VARCHAR(255) NOT NULL,
    method VARCHAR(10) NOT NULL,
    
    -- 状态
    status_code INTEGER,
    is_success BOOLEAN,
    
    -- 性能
    request_started_at TIMESTAMP WITH TIME ZONE,
    request_ended_at TIMESTAMP WITH TIME ZONE,
    duration_ms INTEGER,
    
    -- 错误
    error_code VARCHAR(50),
    error_message TEXT,
    
    -- 元数据 (脱敏)
    metadata JSONB,
    
    -- 时间戳
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 索引
CREATE INDEX idx_logs_request ON api_logs(request_id);
CREATE INDEX idx_logs_connection ON api_logs(connection_id);
CREATE INDEX idx_logs_provider ON api_logs(provider);
CREATE INDEX idx_logs_created ON api_logs(created_at DESC);

3. 视图

3.1 invoice_summary - 发票摘要视图

CREATE VIEW invoice_summary AS
SELECT 
    i.id,
    i.connection_id,
    i.provider,
    i.original_filename,
    i.status,
    i.extraction_confidence,
    i.extracted_supplier_name,
    i.extracted_invoice_number,
    i.extracted_invoice_date,
    i.extracted_amount_total,
    i.extracted_currency,
    i.supplier_number,
    i.supplier_match_confidence,
    i.voucher_number,
    i.voucher_series,
    i.created_at,
    i.processed_at,
    ac.company_name as connection_company,
    CASE 
        WHEN i.status = 'imported' THEN true
        ELSE false
    END as is_imported
FROM invoices i
JOIN accounting_connections ac ON i.connection_id = ac.id;

3.2 connection_stats - 连接统计视图

CREATE VIEW connection_stats AS
SELECT 
    ac.id as connection_id,
    ac.provider,
    ac.company_name,
    ac.is_active,
    COUNT(i.id) as total_invoices,
    COUNT(CASE WHEN i.status = 'imported' THEN 1 END) as imported_invoices,
    COUNT(CASE WHEN i.status = 'failed' THEN 1 END) as failed_invoices,
    AVG(i.extraction_confidence) as avg_confidence,
    MAX(i.created_at) as last_invoice_at
FROM accounting_connections ac
LEFT JOIN invoices i ON ac.id = i.connection_id
GROUP BY ac.id, ac.provider, ac.company_name, ac.is_active;

3.3 user_connections_view - 用户连接视图

CREATE VIEW user_connections_view AS
SELECT 
    u.id as user_id,
    u.email,
    ac.id as connection_id,
    ac.provider,
    ac.company_name,
    ac.company_org_number,
    ac.is_active as connection_active,
    ac.created_at as connected_at
FROM users u
LEFT JOIN accounting_connections ac ON u.id = ac.user_id;

4. 函数和触发器

4.1 自动更新时间戳

-- 创建更新函数
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ language 'plpgsql';

-- 应用到各表
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_connections_updated_at BEFORE UPDATE ON accounting_connections
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_invoices_updated_at BEFORE UPDATE ON invoices
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_mappings_updated_at BEFORE UPDATE ON account_mappings
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

4.2 发票状态变更日志

CREATE TABLE invoice_status_history (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    invoice_id UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
    old_status invoice_status,
    new_status invoice_status NOT NULL,
    changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    changed_by UUID REFERENCES users(id)
);

-- 触发器记录状态变更
CREATE OR REPLACE FUNCTION log_invoice_status_change()
RETURNS TRIGGER AS $$
BEGIN
    IF OLD.status IS DISTINCT FROM NEW.status THEN
        INSERT INTO invoice_status_history (invoice_id, old_status, new_status)
        VALUES (NEW.id, OLD.status, NEW.status);
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER invoice_status_change_trigger
    AFTER UPDATE ON invoices
    FOR EACH ROW
    EXECUTE FUNCTION log_invoice_status_change();

4.3 自动设置 provider 字段

-- 当插入发票时,自动从 connection 获取 provider
CREATE OR REPLACE FUNCTION set_invoice_provider()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.provider IS NULL THEN
        SELECT provider INTO NEW.provider
        FROM accounting_connections
        WHERE id = NEW.connection_id;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER set_invoice_provider_trigger
    BEFORE INSERT ON invoices
    FOR EACH ROW
    EXECUTE FUNCTION set_invoice_provider();

5. 初始化数据

-- 默认会计科目映射 (BAS 2024 - Fortnox)
INSERT INTO account_mappings (connection_id, provider, account_code, account_name, vat_rate, description_template, priority) 
SELECT 
    ac.id,
    ac.provider,
    code,
    name,
    vat_rate,
    description_template,
    priority
FROM accounting_connections ac
CROSS JOIN (VALUES
    (2440, 'Leverantörsskulder', 0, 'Faktura {invoice_number}', 0),
    (2610, 'Ingående moms', 25, 'Moms 25%', 0),
    (2620, 'Ingående moms', 12, 'Moms 12%', 0),
    (2630, 'Ingående moms', 6, 'Moms 6%', 0),
    (5460, 'Kontorsmaterial', 25, 'Kontorsmaterial', 1),
    (5710, 'Frakter', 25, 'Frakt', 1),
    (6100, 'Övriga externa tjänster', 25, 'Övriga tjänster', 1),
    (6210, 'Konsultarvoden', 25, 'Konsult', 2)
) AS defaults(code, name, vat_rate, description_template, priority)
WHERE ac.provider = 'fortnox';

6. 迁移脚本

6.1 从 v1.0 迁移到 v2.0

-- 迁移脚本: 从单 Fortnox 支持到多会计系统支持

-- 1. 创建新表 accounting_connections
CREATE TABLE accounting_connections (
    -- ... (见 2.2 节定义)
);

-- 2. 迁移 fortnox_tenants 数据到 accounting_connections
INSERT INTO accounting_connections (
    id, user_id, provider, access_token_encrypted, refresh_token_encrypted,
    expires_at, scope, company_name, company_org_number,
    default_voucher_series, default_account_code, auto_attach_pdf, auto_create_supplier,
    is_active, last_sync_at, created_at, updated_at
)
SELECT 
    id, user_id, 'fortnox', access_token_encrypted, refresh_token_encrypted,
    expires_at, scope, company_name, company_org_number,
    default_voucher_series, default_account_code, auto_attach_pdf, auto_create_supplier,
    is_active, last_sync_at, created_at, updated_at
FROM fortnox_tenants;

-- 3. 创建新表 invoices
CREATE TABLE invoices (
    -- ... (见 2.3 节定义)
);

-- 4. 迁移 fortnox_invoices 数据到 invoices
INSERT INTO invoices (
    id, connection_id, provider, original_filename, storage_path, file_size, file_hash,
    extraction_data, extraction_confidence,
    extracted_supplier_name, extracted_supplier_org_number, extracted_invoice_number,
    extracted_invoice_date, extracted_due_date, extracted_amount_total, extracted_amount_vat,
    extracted_vat_rate, extracted_ocr_number, extracted_bankgiro, extracted_plusgiro, extracted_currency,
    supplier_number, supplier_match_confidence, supplier_match_action,
    voucher_series, voucher_number, voucher_url, voucher_rows,
    status, error_message, error_code,
    reviewed_by, reviewed_at,
    attachment_id, attachment_url,
    created_at, updated_at, processed_at
)
SELECT 
    id, tenant_id, 'fortnox', original_filename, storage_path, file_size, file_hash,
    extraction_data, extraction_confidence,
    extracted_supplier_name, extracted_supplier_org_number, extracted_invoice_number,
    extracted_invoice_date, extracted_due_date, extracted_amount_total, extracted_amount_vat,
    extracted_vat_rate, extracted_ocr_number, extracted_bankgiro, extracted_plusgiro, extracted_currency,
    supplier_number, supplier_match_confidence, supplier_match_action,
    voucher_series, voucher_number, voucher_url, voucher_rows,
    status, error_message, error_code,
    reviewed_by, reviewed_at,
    attachment_id, attachment_url,
    created_at, updated_at, processed_at
FROM fortnox_invoices;

-- 5. 更新外键引用
-- 更新 supplier_cache
ALTER TABLE supplier_cache DROP CONSTRAINT supplier_cache_tenant_id_fkey;
ALTER TABLE supplier_cache RENAME COLUMN tenant_id TO connection_id;
ALTER TABLE supplier_cache ADD CONSTRAINT supplier_cache_connection_id_fkey 
    FOREIGN KEY (connection_id) REFERENCES accounting_connections(id) ON DELETE CASCADE;

-- 6. 删除旧表 (在确认迁移成功后)
-- DROP TABLE fortnox_invoices;
-- DROP TABLE fortnox_tenants;

-- 7. 创建新索引和视图
-- ... (见相应章节)

6.2 初始迁移 (新部署)

-- 启用 UUID 扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- 创建枚举类型
CREATE TYPE invoice_status AS ENUM ('pending', 'uploading', 'processing', 'preview', 'importing', 'imported', 'failed');
CREATE TYPE supplier_match_action AS ENUM ('USE_EXISTING', 'CREATE_NEW', 'SUGGEST_MATCH');
CREATE TYPE queue_status AS ENUM ('queued', 'processing', 'completed', 'failed', 'cancelled');

-- 创建表 (按依赖顺序)
-- 1. users
-- 2. accounting_connections
-- 3. invoices
-- 4. supplier_cache
-- 5. invoice_reviews
-- 6. processing_queue
-- 7. account_mappings
-- 8. api_logs
-- 9. invoice_status_history

-- 创建索引
-- 创建视图
-- 创建函数和触发器

7. 性能优化建议

7.1 分区策略

对于 invoicesapi_logs 表,建议按时间分区:

-- 按月分区 (示例)
CREATE TABLE invoices_partitioned (
    LIKE invoices INCLUDING ALL
) PARTITION BY RANGE (created_at);

-- 创建分区
CREATE TABLE invoices_2024_01 PARTITION OF invoices_partitioned
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE invoices_2024_02 PARTITION OF invoices_partitioned
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

7.2 多会计系统查询优化

-- 为常用查询创建复合索引
CREATE INDEX idx_invoices_provider_status ON invoices(provider, status);
CREATE INDEX idx_invoices_connection_created ON invoices(connection_id, created_at DESC);

-- 为 provider 统计查询优化
CREATE INDEX idx_connections_user_provider ON accounting_connections(user_id, provider);

7.3 清理策略

-- 定期清理过期缓存
DELETE FROM supplier_cache WHERE expires_at < NOW();

-- 归档旧日志
-- 建议将 90 天前的 api_logs 迁移到冷存储
CREATE TABLE api_logs_archive (LIKE api_logs INCLUDING ALL);

-- 归档脚本
INSERT INTO api_logs_archive 
SELECT * FROM api_logs 
WHERE created_at < NOW() - INTERVAL '90 days';

DELETE FROM api_logs 
WHERE created_at < NOW() - INTERVAL '90 days';

8. 备份策略

数据类型 备份频率 保留期
全量备份 每日 30 天
增量备份 每小时 7 天
事务日志 持续 7 天
-- 关键表逻辑备份示例
pg_dump --table=accounting_connections --table=invoices --table=users > backup.sql

-- 按 provider 备份
pg_dump --table=invoices --where="provider = 'fortnox'" > fortnox_invoices_backup.sql

9. EF Core 迁移

9.1 创建迁移

# 添加迁移
cd backend/src/InvoiceMaster.Infrastructure
dotnet ef migrations add InitialCreate --startup-project ../InvoiceMaster.API

# 更新数据库
dotnet ef database update --startup-project ../InvoiceMaster.API

# 生成 SQL 脚本
dotnet ef migrations script --startup-project ../InvoiceMaster.API -o migration.sql

9.2 领域事件存储配置

// InvoiceMaster.Infrastructure/Data/Configurations/DomainEventConfiguration.cs
public class DomainEventConfiguration : IEntityTypeConfiguration<DomainEvent>
{
    public void Configure(EntityTypeBuilder<DomainEvent> builder)
    {
        builder.ToTable("DomainEvents");
        
        builder.HasKey(e => e.Id);
        
        builder.Property(e => e.EventType)
            .IsRequired()
            .HasMaxLength(255);
            
        builder.Property(e => e.AggregateType)
            .IsRequired()
            .HasMaxLength(255);
            
        builder.Property(e => e.Payload)
            .IsRequired()
            .HasColumnType("jsonb");
            
        builder.HasIndex(e => new { e.AggregateType, e.AggregateId })
            .HasDatabaseName("IX_DomainEvents_Aggregate");
            
        builder.HasIndex(e => e.OccurredAt)
            .HasDatabaseName("IX_DomainEvents_OccurredAt");
    }
}

10. Schema 变更日志

v3.0 (2026-02-03)

技术栈变更:

  • Python/SQLAlchemy → .NET 8/EF Core 8
  • 新增 DomainEvents 表用于审计

新增表:

  • DomainEvents - 领域事件存储(审计)

EF Core 配置:

  • 所有表使用 Fluent API 配置
  • 启用全局查询过滤器(软删除)
  • 配置领域事件拦截器

v2.0 (2026-02-03)

新增表:

  • accounting_connections - 替换 fortnox_tenants,支持多会计系统
  • invoices - 替换 fortnox_invoices,添加 provider 字段

修改表:

  • supplier_cache: tenant_idconnection_id
  • account_mappings: 添加 connection_idprovider 字段
  • api_logs: tenant_idconnection_idapi_nameprovider

新增视图:

  • connection_stats - 替换 tenant_stats
  • user_connections_view - 用户连接视图

新增触发器:

  • set_invoice_provider_trigger - 自动设置 provider 字段

文档历史:

版本 日期 作者 变更
3.0 2026-02-03 Claude Code 重构为 .NET + EF Core + 审计支持
2.0 2026-02-03 Claude Code 重构为多会计系统 Schema
1.0 2026-02-03 Claude Code 初始版本