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

880 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 - 用户表
存储系统用户基本信息。
```sql
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 连接信息,一个用户可以连接多个会计系统。
```sql
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 - 发票处理记录表
存储发票处理历史和状态,支持多会计系统。
```sql
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 调用。
```sql
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 结果的修改和确认。
```sql
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 - 处理队列表
异步任务队列,用于发票处理。
```sql
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 - 科目映射表
存储用户自定义的会计科目映射规则,支持多会计系统。
```sql
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 调用用于审计和调试。
```sql
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 - 发票摘要视图
```sql
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 - 连接统计视图
```sql
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 - 用户连接视图
```sql
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 自动更新时间戳
```sql
-- 创建更新函数
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 发票状态变更日志
```sql
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 字段
```sql
-- 当插入发票时,自动从 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. 初始化数据
```sql
-- 默认会计科目映射 (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
```sql
-- 迁移脚本: 从单 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 初始迁移 (新部署)
```sql
-- 启用 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` 表,建议按时间分区:
```sql
-- 按月分区 (示例)
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 多会计系统查询优化
```sql
-- 为常用查询创建复合索引
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 清理策略
```sql
-- 定期清理过期缓存
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 天 |
```sql
-- 关键表逻辑备份示例
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 创建迁移
```bash
# 添加迁移
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 领域事件存储配置
```csharp
// 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_id`
- `account_mappings`: 添加 `connection_id``provider` 字段
- `api_logs`: `tenant_id``connection_id``api_name``provider`
**新增视图:**
- `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 | 初始版本 |