- 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
28 KiB
28 KiB
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 分区策略
对于 invoices 和 api_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_id→connection_idaccount_mappings: 添加connection_id和provider字段api_logs:tenant_id→connection_id,api_name→provider
新增视图:
connection_stats- 替换tenant_statsuser_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 | 初始版本 |