# Invoice Master - Fortnox Integration Technical Specification **版本**: v1.0 **日期**: 2026-02-01 **作者**: Claude Code **状态**: 设计阶段 --- ## 目录 1. [概述](#概述) 2. [集成模式说明](#集成模式说明) 3. [系统架构](#系统架构) 4. [Fortnox API分析](#fortnox-api分析) 5. [数据映射设计](#数据映射设计) 6. [核心功能模块](#核心功能模块) 7. [用户流程设计](#用户流程设计) 8. [UI设计规范](#ui设计规范) 9. [API设计](#api设计) 10. [数据库设计](#数据库设计) 11. [安全设计](#安全设计) 12. [错误处理](#错误处理) 13. [开发计划](#开发计划) 14. [测试策略](#测试策略) 15. [部署方案](#部署方案) 16. [附录](#附录) --- ## 概述 ### 1.1 项目背景 Invoice Master是一个基于YOLOv11 + PaddleOCR的发票字段自动提取系统,当前准确率达到94.8%。本方案设计将Invoice Master作为Fortnox会计软件的插件/扩展,实现无缝的发票数据导入功能。 ### 1.2 目标 - 为Fortnox用户提供智能发票识别功能 - 实现一键将发票数据导入Fortnox - 自动匹配供应商和会计科目 - 减少90%的手动录入工作 ### 1.3 范围 **包含功能:** - Fortnox OAuth2认证集成 - 发票PDF上传和OCR识别 - 供应商自动匹配/创建 - 会计凭证(Voucher)自动生成 - 发票图像存档 **不包含功能 (Phase 2):** - 多文档类型支持 (收据、对账单) - 自动付款流程 - 审批工作流 ### 1.4 术语定义 | 术语 | 英文 | 说明 | |------|------|------| | 供应商 | Supplier | Leverantör i Fortnox | | 会计凭证 | Voucher | Verifikation i Fortnox | | 发票 | Invoice | Faktura | | 科目 | Account | Konto i kontoplanen | | OCR参考号 | OCR Number | 瑞典特有的付款参考号 | --- ## 集成模式说明 ### 2.1 Fortnox Extension UI模式 Fortnox的集成主要有**两种模式**,Invoice Master采用**模式1: 外部独立应用**。 #### 模式1: 外部独立应用 (External App) - 推荐 **架构示意图:** ``` 用户流程: ┌─────────────────┐ ┌─────────────────────┐ ┌─────────────────┐ │ Fortnox │────▶│ Invoice Master │────▶│ Fortnox │ │ (点击集成) │ │ (独立Web应用) │ │ (数据已导入) │ └─────────────────┘ └─────────────────────┘ └─────────────────┘ │ │ │ │ │ │ ▼ ▼ ▼ 在Fortnox中 用户在你的网站上 用户回到Fortnox 看到"Invoice Master" 完成发票上传和识别 查看已导入的凭证 点击打开新窗口 ``` **特点:** - ✅ **有自己的完整UI**(独立网站) - ✅ 通过OAuth2连接Fortnox - ✅ 用户在Fortnox点击后跳转到你的网站 - ✅ 数据通过API双向同步 - ✅ 更灵活的功能和用户体验 **Fortnox中的展示:** - 在Fortnox Integrations页面列出 - 用户点击后打开新标签页到你的网站 - 显示连接状态和基本设置 #### 模式2: 嵌入式集成 (Embedded) - 有限支持 **Fortnox目前支持:** - 菜单链接 (Menu Links) - 在Fortnox菜单中添加链接 - 快捷操作 (Quick Actions) - 有限的上下文操作 - 文件导入 (File Import) - 通过Inbox API **Fortnox不提供:** - ❌ iframe嵌入第三方UI - ❌ 自定义页面/标签 - ❌ 深度UI定制 ### 2.2 推荐方案: 混合模式 **架构设计:** ``` ┌─────────────────────────────────────────────────────────────────┐ │ Invoice Master for Fortnox │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 独立Web应用 (你的域名) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ │ │ 发票上传 │ │ 识别结果 │ │ 历史记录 │ │ │ │ │ │ 页面 │ │ 确认页面 │ │ 页面 │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ 功能: OCR识别、供应商匹配、预览确认、一键导入Fortnox │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ │ │ HTTPS API │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Fortnox Integration Service │ │ │ │ (Backend API) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ └──────────────────────────────┼───────────────────────────────────┘ │ │ OAuth2 + REST API │ ┌──────────▼──────────┐ │ Fortnox │ │ (数据存储/展示) │ └─────────────────────┘ ``` ### 2.3 与纯API方案对比 | 特性 | 独立UI方案 (推荐) | 纯API方案 | |------|------------------|-----------| | **用户体验** | ⭐⭐⭐⭐⭐ 完整的可视化界面 | ⭐⭐ 需要用户自己调用API | | **开发复杂度** | ⭐⭐⭐ 需要前端+后端 | ⭐⭐ 只需要后端API | | **功能灵活性** | ⭐⭐⭐⭐⭐ 可以做OCR预览、编辑 | ⭐⭐ 直接导入,无法预览 | | **用户门槛** | ⭐⭐⭐⭐⭐ 低,非技术用户可用 | ⭐ 高,需要开发者 | | **Fortnox审核** | ⭐⭐⭐⭐ 标准流程 | ⭐⭐⭐⭐ 更简单 | ### 2.4 用户完整流程 ``` 1. 发现阶段 用户在Fortnox Integrations页面找到"Invoice Master" 2. 授权阶段 用户点击"连接" → OAuth2授权 → 跳转到Invoice Master 3. 使用阶段 (在Invoice Master网站) 上传PDF → OCR识别 → 确认/编辑 → 导入到Fortnox 4. 查看阶段 (回到Fortnox) 用户在Fortnox中查看已导入的凭证和发票 ``` --- ## 系统架构 ### 3.1 整体架构图 **注意: 这是技术架构图,对应第2章描述的"独立Web应用"模式** ``` ┌─────────────────────────────────────────────────────────────────┐ │ Fortnox Platform │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Fortnox │ │ Fortnox │ │ Fortnox │ │ │ │ UI │ │ API │ │ Database │ │ │ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │ └─────────┼─────────────────┼─────────────────────────────────────┘ │ │ │ OAuth2 │ HTTPS │ │ ┌─────────▼─────────────────▼─────────────────────────────────────┐ │ Invoice Master Integration │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Fortnox Integration Service │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ │ │ Auth │ │ Invoice │ │ Supplier │ │ │ │ │ │ Module │ │ Handler │ │ Matcher │ │ │ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ │ │ Voucher │ │ File │ │ Webhook │ │ │ │ │ │ Creator │ │ Storage │ │ Handler │ │ │ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ Invoice Master Core Services │ │ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ │ │ OCR │ │ YOLO │ │ Field │ │ │ │ │ │ Engine │ │ Detector │ │Normalizer │ │ │ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │ └──────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ │ PostgreSQL / Azure Blob │ ┌─────────▼─────────────────────────────────────────────────────┐ │ Data Storage │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Invoice │ │ Fortnox │ │ File │ │ │ │ Data │ │ Tokens │ │ Storage │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 3.2 组件说明 | 组件 | 技术栈 | 职责 | |------|--------|------| | **Integration Service** | FastAPI + Python | Fortnox API交互、业务逻辑 | | **Auth Module** | OAuth2 + JWT | Fortnox认证、Token管理 | | **Invoice Handler** | - | 发票处理流程协调 | | **Supplier Matcher** | Fuzzy Matching | 供应商匹配算法 | | **Voucher Creator** | - | 生成Fortnox会计凭证 | | **File Storage** | Azure Blob / S3 | 发票PDF存储 | | **Webhook Handler** | - | 接收Fortnox事件 | ### 3.3 技术栈 | 层级 | 技术 | 说明 | |------|------|------| | **Backend** | FastAPI + Python 3.11 | API服务 | | **Database** | PostgreSQL 15 | 关系数据 | | **Cache** | Redis | Token缓存、限流 | | **Storage** | Azure Blob Storage | 文件存储 | | **Message Queue** | Redis Queue | 异步任务 | | **Monitoring** | Prometheus + Grafana | 监控告警 | --- ## Fortnox API分析 ### 3.1 认证机制 Fortnox使用OAuth2授权码流程: ``` ┌─────────┐ ┌─────────────┐ │ User │──(1) Authorization Request───────▶│ Fortnox │ │ │◀──(2) Authorization Code─────────│ OAuth2 │ │ │ │ Server │ │ │──(3) Token Request────────────────▶│ │ │ │◀──(4) Access + Refresh Token──────│ │ └─────────┘ └─────────────┘ ``` **关键端点:** ``` Authorization URL: https://apps.fortnox.se/oauth-v1/auth Token URL: https://apps.fortnox.se/oauth-v1/token API Base URL: https://api.fortnox.se/3 ``` **Scopes Required:** ``` supplier - 供应商管理 invoice - 发票管理 (如需要) voucher - 会计凭证 account - 会计科目 companyinformation - 公司信息 ``` ### 3.2 核心API端点 #### 3.2.1 供应商管理 ```http # 获取供应商列表 GET /3/suppliers Response: { "Suppliers": [ { "@url": "https://api.fortnox.se/3/suppliers/123", "Name": "ABC Company", "SupplierNumber": "123", "OrganisationNumber": "556677-8899" } ] } # 创建供应商 POST /3/suppliers Body: { "Supplier": { "Name": "New Supplier", "OrganisationNumber": "112233-4455" } } ``` #### 3.2.2 会计凭证 (Voucher) ```http # 创建会计凭证 POST /3/vouchers Body: { "Voucher": { "VoucherSeries": "A", // 凭证系列 "TransactionDate": "2024-01-15", // 交易日期 "VoucherRows": [ { "Account": 2440, // 应付账款科目 "Debit": 1250.00, "Credit": 0, "Description": "Invoice F2024-001" }, { "Account": 5460, // 费用科目 "Debit": 0, "Credit": 1000.00, "Description": "Office supplies" }, { "Account": 2610, // 增值税科目 "Debit": 0, "Credit": 250.00, "Description": "VAT 25%" } ] } } ``` #### 3.2.3 文件上传 ```http # 上传附件到Fortnox POST /3/inbox Content-Type: multipart/form-data Body: { "file": [PDF file], "name": "Invoice_F2024_001.pdf" } ``` ### 3.3 API限制 | 限制类型 | 值 | 说明 | |---------|-----|------| | 速率限制 | 300请求/分钟 | 超出返回429 | | 并发连接 | 10 | 同时连接数 | | Token有效期 | 3600秒 | 需使用Refresh Token | | 文件大小 | 10MB | 单个文件限制 | --- ## 数据映射设计 ### 4.1 发票字段映射 **Invoice Master提取字段 → Fortnox字段** | Invoice Master | Fortnox | 类型 | 必填 | 转换逻辑 | |---------------|---------|------|------|----------| | `invoice_number` | `ExternalInvoiceNumber` | string | 是 | 直接映射 | | `invoice_date` | `TransactionDate` | date | 是 | ISO 8601格式 | | `due_date` | `DueDate` | date | 否 | 计算或提取 | | `supplier_name` | `SupplierName` | string | 是 | 匹配或创建 | | `supplier_org_number` | `SupplierOrganisationNumber` | string | 否 | 用于匹配 | | `amount_total` | `TotalAmount` | decimal | 是 | 直接映射 | | `amount_vat` | `VatAmount` | decimal | 否 | 计算得出 | | `ocr_number` | `OCRNumber` | string | 否 | 瑞典特有 | | `bankgiro` | `BankgiroNumber` | string | 否 | 付款信息 | | `plusgiro` | `PlusgiroNumber` | string | 否 | 付款信息 | | `currency` | `Currency` | string | 是 | 默认SEK | ### 4.2 会计科目映射 **默认科目映射表 (Kontoplan BAS2024)** | 费用类型 | 科目代码 | 科目名称 | 说明 | |---------|---------|---------|------| | 应付账款 | 2440 | Leverantörsskulder | 默认贷方 | | 办公用品 | 5460 | Kontorsmaterial | 常见费用 | | 咨询服务 | 6210 | Konsultarvoden | 外部服务 | | 运输费 | 5710 | Frakter | 物流费用 | | 增值税进项 | 2610 | Ingående moms | 25% VAT | | 增值税进项12% | 2620 | Ingående moms 12% | 食品等 | | 增值税进项6% | 2630 | Ingående moms 6% | 交通等 | **科目选择逻辑:** ```python def select_account(invoice_data: dict) -> int: """根据发票内容选择会计科目""" # 1. 检查是否有历史映射 if invoice_data['supplier_org_number']: historical = get_historical_account(invoice_data['supplier_org_number']) if historical: return historical # 2. 关键词匹配 description = invoice_data.get('description', '').lower() if any(word in description for word in ['kontor', 'papper', 'penna']): return 5460 # 办公用品 elif any(word in description for word in ['konsult', 'tjänst']): return 6210 # 咨询服务 elif any(word in description for word in ['frakt', 'transport']): return 5710 # 运输费 # 3. 默认科目 return 6100 # 其他外部费用 ``` ### 4.3 供应商匹配算法 **匹配优先级:** ```python class SupplierMatcher: def match_supplier(self, extracted_data: dict) -> MatchResult: """ 供应商匹配算法 返回: (supplier_number, confidence_score, action) """ # 1. 组织号精确匹配 (权重: 100%) if extracted_data.get('supplier_org_number'): exact_match = self.find_by_org_number( extracted_data['supplier_org_number'] ) if exact_match: return MatchResult( supplier_number=exact_match.number, confidence=1.0, action='USE_EXISTING' ) # 2. 名称模糊匹配 (权重: 80%) name_matches = self.fuzzy_match_name( extracted_data['supplier_name'], threshold=0.85 ) if name_matches and name_matches[0].score > 0.9: return MatchResult( supplier_number=name_matches[0].number, confidence=name_matches[0].score, action='USE_EXISTING' ) # 3. 建议创建新供应商 (权重: <80%) return MatchResult( supplier_number=None, confidence=0.0, action='CREATE_NEW', suggested_name=extracted_data['supplier_name'] ) ``` --- ## 核心功能模块 ### 5.1 认证模块 (Auth Module) **职责:** - Fortnox OAuth2流程管理 - Token存储和刷新 - 多租户隔离 **核心类:** ```python class FortnoxAuthManager: """Fortnox认证管理器""" def __init__(self, client_id: str, client_secret: str): self.client_id = client_id self.client_secret = client_secret self.token_store = TokenStore() def get_authorization_url(self, state: str) -> str: """生成Fortnox授权URL""" params = { 'client_id': self.client_id, 'redirect_uri': settings.FORTNOX_REDIRECT_URI, 'scope': 'supplier voucher account companyinformation', 'state': state, 'response_type': 'code' } return f"{FORTNOX_AUTH_URL}?{urlencode(params)}" async def exchange_code_for_token(self, code: str) -> FortnoxToken: """用授权码换取Token""" response = await httpx.post( FORTNOX_TOKEN_URL, auth=(self.client_id, self.client_secret), data={ 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': settings.FORTNOX_REDIRECT_URI } ) token_data = response.json() return FortnoxToken( access_token=token_data['access_token'], refresh_token=token_data['refresh_token'], expires_at=datetime.utcnow() + timedelta(seconds=token_data['expires_in']), scope=token_data['scope'] ) async def get_valid_access_token(self, tenant_id: str) -> str: """获取有效的访问Token(自动刷新)""" token = await self.token_store.get(tenant_id) if token.is_expired(): token = await self.refresh_token(token.refresh_token) await self.token_store.save(tenant_id, token) return token.access_token ``` ### 5.2 发票处理模块 (Invoice Handler) **处理流程:** ```python class InvoiceProcessingService: """发票处理服务""" async def process_invoice( self, tenant_id: str, pdf_file: UploadFile, settings: ProcessingSettings ) -> ProcessingResult: """ 处理发票的主流程 """ # 1. 保存PDF文件 file_path = await self.file_storage.save(pdf_file) # 2. OCR提取 extraction_result = await self.ocr_service.extract(file_path) # 3. 验证提取结果 if not self.validate_extraction(extraction_result): return ProcessingResult( status='FAILED', error='Extraction validation failed' ) # 4. 供应商匹配 supplier_match = await self.supplier_matcher.match( tenant_id, extraction_result ) # 5. 创建或获取供应商 if supplier_match.action == 'CREATE_NEW': supplier_number = await self.create_supplier( tenant_id, extraction_result ) else: supplier_number = supplier_match.supplier_number # 6. 生成会计凭证 voucher = await self.voucher_creator.create( tenant_id, extraction_result, supplier_number, settings ) # 7. 上传附件 if settings.attach_pdf: await self.attach_invoice_pdf(tenant_id, voucher.id, file_path) return ProcessingResult( status='SUCCESS', extraction=extraction_result, supplier_number=supplier_number, voucher_id=voucher.id, confidence=supplier_match.confidence ) ``` ### 5.3 供应商匹配模块 (Supplier Matcher) ```python class FortnoxSupplierMatcher: """Fortnox供应商匹配器""" def __init__(self, fortnox_client: FortnoxClient): self.client = fortnox_client self.cache = SupplierCache() async def match( self, tenant_id: str, extraction: ExtractionResult ) -> SupplierMatchResult: """匹配供应商""" # 获取所有供应商(带缓存) suppliers = await self.cache.get_suppliers(tenant_id) # 1. 组织号精确匹配 if extraction.supplier_org_number: match = self._match_by_org_number( suppliers, extraction.supplier_org_number ) if match: return SupplierMatchResult( supplier_number=match['SupplierNumber'], confidence=1.0, action='USE_EXISTING' ) # 2. 名称模糊匹配 name_match = self._fuzzy_match_name( suppliers, extraction.supplier_name ) if name_match and name_match['score'] > 0.9: return SupplierMatchResult( supplier_number=name_match['supplier']['SupplierNumber'], confidence=name_match['score'], action='USE_EXISTING' ) elif name_match and name_match['score'] > 0.7: return SupplierMatchResult( supplier_number=name_match['supplier']['SupplierNumber'], confidence=name_match['score'], action='SUGGEST_MATCH', suggested_name=extraction.supplier_name ) # 3. 建议创建新供应商 return SupplierMatchResult( supplier_number=None, confidence=0.0, action='CREATE_NEW', suggested_name=extraction.supplier_name, suggested_org_number=extraction.supplier_org_number ) async def create_supplier( self, tenant_id: str, extraction: ExtractionResult ) -> str: """在Fortnox中创建新供应商""" supplier_data = { 'Supplier': { 'Name': extraction.supplier_name, 'OrganisationNumber': extraction.supplier_org_number, 'Address1': extraction.supplier_address, 'Phone': extraction.supplier_phone, 'Email': extraction.supplier_email, 'BankgiroNumber': extraction.bankgiro, 'PlusgiroNumber': extraction.plusgiro } } response = await self.client.post( tenant_id, '/3/suppliers', json=supplier_data ) # 刷新缓存 await self.cache.invalidate(tenant_id) return response['Supplier']['SupplierNumber'] ``` ### 5.4 凭证生成模块 (Voucher Creator) ```python class FortnoxVoucherCreator: """Fortnox会计凭证生成器""" async def create_voucher( self, tenant_id: str, extraction: ExtractionResult, supplier_number: str, settings: VoucherSettings ) -> VoucherResult: """创建会计凭证""" # 确定会计科目 account = await self.select_account(extraction) # 计算VAT vat_amount = self.calculate_vat( extraction.amount_total, extraction.vat_rate or 25 ) amount_excl_vat = extraction.amount_total - vat_amount # 构建凭证行 voucher_rows = [ # 借方: 费用科目 { 'Account': account, 'Debit': amount_excl_vat, 'Credit': 0, 'Description': f"{extraction.supplier_name} - {extraction.invoice_number}", 'Project': settings.project_code }, # 借方: 增值税 { 'Account': self.get_vat_account(extraction.vat_rate), 'Debit': vat_amount, 'Credit': 0, 'Description': f"Moms {extraction.vat_rate}%" }, # 贷方: 应付账款 { 'Account': 2440, # Leverantörsskulder 'Debit': 0, 'Credit': extraction.amount_total, 'Description': f"Faktura {extraction.invoice_number}", 'SupplierNumber': supplier_number, 'OCRNumber': extraction.ocr_number } ] voucher_data = { 'Voucher': { 'VoucherSeries': settings.voucher_series or 'A', 'TransactionDate': extraction.invoice_date.isoformat(), 'VoucherText': f"Inköp {extraction.supplier_name}", 'VoucherRows': voucher_rows } } response = await self.client.post( tenant_id, '/3/vouchers', json=voucher_data ) return VoucherResult( voucher_id=response['Voucher']['VoucherNumber'], series=response['Voucher']['VoucherSeries'], url=response['Voucher']['@url'] ) ``` --- ## 用户流程设计 ### 6.1 集成入口点 (在Fortnox中) **Fortnox Integrations页面展示:** ``` ┌─────────────────────────────────────────────────────────────┐ │ Invoice Master - Smart Invoice OCR │ │ │ │ 📄 自动识别发票信息 │ │ 🤖 AI驱动的OCR技术 │ │ ⚡ 一键导入到Fortnox │ │ │ │ [连接/打开] │ └─────────────────────────────────────────────────────────────┘ ``` **Fortnox内配置页面:** ``` ┌─────────────────────────────────────────────────────────────┐ │ Invoice Master 设置 │ │ │ │ 状态: ✅ 已连接 │ │ 公司: My Company AB │ │ │ │ 默认设置: │ │ - 凭证系列: [A ▼] │ │ - 自动导入: [✓] │ │ - 附件上传: [✓] │ │ │ │ [保存设置] [断开连接] │ └─────────────────────────────────────────────────────────────┘ ``` ### 6.2 首次设置流程 ``` 用户点击"连接Fortnox" │ ▼ ┌───────────────────┐ │ 跳转到Fortnox授权页 │ │ (OAuth2流程) │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ 用户登录Fortnox │ │ 并授权访问 │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ 返回到Invoice │ │ Master回调页面 │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ 获取公司信息 │ │ 验证连接成功 │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ 配置默认设置 │ │ - 凭证系列 │ │ - 默认科目 │ │ - 文件存储选项 │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ 完成!显示 │ │ 上传发票界面 │ └───────────────────┘ ``` ### 6.2 发票处理流程 ``` 用户上传PDF发票 │ ▼ ┌───────────────────┐ │ 显示处理进度 │ │ - OCR提取中... │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ 显示提取结果 │ │ 供用户确认/编辑 │ │ │ │ ┌───────────────┐ │ │ │ 供应商: XXX │ │ │ │ 金额: 1,250 │ │ │ │ 日期: 2024... │ │ │ │ [编辑] [确认] │ │ │ └───────────────┘ │ └─────────┬─────────┘ │ ┌─────┴─────┐ │ │ ▼ ▼ ┌────────┐ ┌────────┐ │ 编辑 │ │ 确认 │ │ 数据 │ │ 导入 │ └───┬────┘ └───┬────┘ │ │ └─────┬─────┘ │ ▼ ┌───────────────────┐ │ 供应商匹配 │ │ - 查找现有 │ │ - 或创建新 │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ 生成会计凭证 │ │ 上传到Fortnox │ └─────────┬─────────┘ │ ▼ ┌───────────────────┐ │ 显示成功消息 │ │ 提供Fortnox链接 │ │ 查看凭证 │ └───────────────────┘ ``` ### 6.4 独立Web应用UI设计 **重要说明: 以下UI是Invoice Master独立Web应用的界面,用户在Fortnox点击"打开"后跳转到此界面。** #### 主界面 #### 主界面 ``` ┌─────────────────────────────────────────────────────────────┐ │ Invoice Master for Fortnox [⚙️设置] [?] │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 📤 上传发票 │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 拖放PDF文件到这里 │ │ │ │ 或点击选择文件 │ │ │ │ │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ 📋 最近处理的发票 │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 文件名 │ 供应商 │ 金额 │ 状态 │ │ │ ├───────────────────────────────────────────────────────┤ │ │ │ INV001.pdf │ ABC Company │ 1,250 │ ✅ 已导入 │ │ │ │ INV002.pdf │ XYZ AB │ 3,450 │ ✅ 已导入 │ │ │ │ INV003.pdf │ (未匹配) │ 890 │ ⚠️ 待确认 │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` #### 结果确认界面 **说明: 此界面在Invoice Master独立Web应用中显示,用于用户确认OCR识别结果。 ``` ┌─────────────────────────────────────────────────────────────┐ │ 确认发票信息 [✕] [✓] │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 📄 Invoice_F2024_001.pdf │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 供应商信息 │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ 名称: ABC Company │ │ │ │ │ │ 组织号: 556677-8899 │ │ │ │ │ │ 状态: ✅ 已匹配现有供应商 │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 发票信息 │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ 发票号: F2024-001 │ │ │ │ │ │ 日期: 2024-01-15 │ │ │ │ │ │ 到期日: 2024-02-15 │ │ │ │ │ │ 金额: 1,250.00 SEK │ │ │ │ │ │ OCR: 7350012345678 │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ 会计科目 │ │ │ │ 借方: 5460 - Kontorsmaterial 1,000.00 │ │ │ │ 借方: 2610 - Ingående moms 250.00 │ │ │ │ 贷方: 2440 - Leverantörsskulder 1,250.00 │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ [编辑信息] [重新识别] [取消] [确认并导入到Fortnox] │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## UI设计规范 ### 7.1 设计原则 **独立Web应用设计原则:** 1. **品牌一致性**: 保持Invoice Master品牌,同时尊重Fortnox用户习惯 2. **简洁高效**: 发票处理是高频操作,界面必须简洁快速 3. **清晰反馈**: OCR识别结果必须清晰展示,便于用户确认 4. **无缝集成**: 虽然是独立应用,但要让用户感觉与Fortnox是一体的 ### 7.2 响应式设计 **断点定义:** | 断点 | 宽度 | 布局 | |------|------|------| | Mobile | < 768px | 单列,堆叠布局 | | Tablet | 768px - 1024px | 双列布局 | | Desktop | > 1024px | 三列布局 | ### 7.3 组件规范 #### 文件上传区域 ``` ┌─────────────────────────────────────────────────────────────┐ │ 📤 上传发票 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 📄 拖放PDF文件到这里 │ │ │ │ │ │ │ │ 或点击选择 │ │ │ │ │ │ │ │ 支持格式: PDF, JPG, PNG (最大10MB) │ │ │ │ │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` **交互状态:** - 默认: 灰色边框,虚线 - 悬停: 蓝色边框,背景变浅蓝 - 拖入: 蓝色边框,背景变深蓝 - 上传中: 显示进度条 #### 发票卡片 ``` ┌─────────────────────────────────────────────────────────────┐ │ 📄 Invoice_F2024_001.pdf [✓] [✏️] [🗑️]│ ├─────────────────────────────────────────────────────────────┤ │ │ │ 供应商: ABC Company (556677-8899) │ │ 金额: 1,250.00 SEK │ │ 日期: 2024-01-15 │ │ │ │ 状态: ✅ 已导入到Fortnox │ │ 凭证: A-1234 [在Fortnox中查看] │ │ │ └─────────────────────────────────────────────────────────────┘ ``` ### 7.4 颜色规范 **主色调:** | 用途 | 颜色 | Hex | |------|------|-----| | 主色 | 蓝色 | #2563EB | | 成功 | 绿色 | #10B981 | | 警告 | 黄色 | #F59E0B | | 错误 | 红色 | #EF4444 | | 背景 | 浅灰 | #F9FAFB | | 文字 | 深灰 | #1F2937 | **Fortnox品牌协调:** - 使用Fortnox的蓝色作为次要色 (#0057FF) - 在"导入到Fortnox"按钮中使用Fortnox品牌色 ### 7.5 字体规范 | 元素 | 字体 | 大小 | 字重 | |------|------|------|------| | 标题 | Inter | 24px | 600 | | 副标题 | Inter | 18px | 500 | | 正文 | Inter | 14px | 400 | | 小字 | Inter | 12px | 400 | | 数字 | Inter | 16px | 600 (等宽) | --- ## API设计 ### 8.1 REST API端点 #### 认证相关 ```http # 获取Fortnox授权URL GET /api/v1/fortnox/auth/url Response: { "authorization_url": "https://apps.fortnox.se/oauth-v1/auth?...", "state": "random_state_string" } # OAuth回调处理 GET /api/v1/fortnox/auth/callback?code=xxx&state=xxx Response: { "status": "success", "company_name": "My Company AB", "connected_at": "2024-01-15T10:30:00Z" } # 断开连接 DELETE /api/v1/fortnox/auth Response: { "status": "disconnected" } ``` #### 发票处理 ```http # 上传并处理发票 POST /api/v1/fortnox/invoices Content-Type: multipart/form-data Body: { "file": [PDF file], "auto_import": false, // 是否自动导入,false则返回预览 "settings": { "voucher_series": "A", "attach_pdf": true } } Response (预览模式): { "id": "uuid", "status": "preview", "extraction": { "supplier_name": "ABC Company", "supplier_org_number": "556677-8899", "invoice_number": "F2024-001", "invoice_date": "2024-01-15", "amount_total": 1250.00, "ocr_number": "7350012345678" }, "supplier_match": { "action": "USE_EXISTING", "supplier_number": "123", "confidence": 1.0 }, "voucher_preview": { "rows": [...] } } Response (自动导入模式): { "id": "uuid", "status": "imported", "voucher": { "voucher_number": "1234", "series": "A", "url": "https://api.fortnox.se/3/vouchers/A/1234" }, "fortnox_url": "https://apps.fortnox.se/..." } ``` #### 供应商管理 ```http # 获取Fortnox供应商列表 GET /api/v1/fortnox/suppliers Response: { "suppliers": [ { "supplier_number": "123", "name": "ABC Company", "organisation_number": "556677-8899" } ] } # 创建供应商 POST /api/v1/fortnox/suppliers Body: { "name": "New Supplier", "organisation_number": "112233-4455", "address": "..." } ``` ### 8.2 Webhook接收 ```http # Fortnox Webhook接收端点 POST /webhooks/fortnox Headers: { "X-Fortnox-Event": "voucher.created" } Body: { "event": "voucher.created", "data": { "voucher_number": "1234", "series": "A" } } ``` --- ## 数据库设计 ### 9.1 实体关系图 ``` ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ fortnox_tenants │ │ fortnox_invoices │ │ supplier_cache │ ├─────────────────┤ ├──────────────────┤ ├─────────────────┤ │ id (PK) │◄──────┤ id (PK) │ │ id (PK) │ │ organization_id │ │ tenant_id (FK) │ │ tenant_id (FK) │ │ access_token │ │ file_path │ │ supplier_number │ │ refresh_token │ │ extraction_data │ │ name │ │ expires_at │ │ voucher_id │ │ org_number │ │ company_name │ │ status │ │ cached_at │ │ created_at │ │ created_at │ └─────────────────┘ └─────────────────┘ └──────────────────┘ │ │ ┌──────────────────┐ │ │ processing_queue │ │ ├──────────────────┤ └────────►│ id (PK) │ │ invoice_id (FK) │ │ status │ │ retry_count │ └──────────────────┘ ``` ### 9.2 SQL Schema ```sql -- Fortnox租户表 CREATE TABLE fortnox_tenants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID NOT NULL REFERENCES organizations(id), -- OAuth Tokens access_token TEXT NOT NULL, refresh_token TEXT NOT NULL, expires_at TIMESTAMP NOT NULL, scope TEXT, -- 公司信息 company_name VARCHAR(255), company_org_number VARCHAR(20), -- 设置 default_voucher_series VARCHAR(10) DEFAULT 'A', default_account_code INTEGER DEFAULT 5460, auto_attach_pdf BOOLEAN DEFAULT true, -- 状态 is_active BOOLEAN DEFAULT true, last_sync_at TIMESTAMP, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), UNIQUE(organization_id) ); -- Fortnox发票处理记录 CREATE TABLE fortnox_invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES fortnox_tenants(id), -- 文件信息 original_filename VARCHAR(255), storage_path TEXT, file_size INTEGER, -- OCR提取结果 extraction_data JSONB, extraction_confidence DECIMAL(3,2), -- 供应商匹配 supplier_number VARCHAR(50), supplier_match_confidence DECIMAL(3,2), supplier_match_action VARCHAR(20), -- USE_EXISTING, CREATE_NEW, SUGGEST_MATCH -- Fortnox凭证 voucher_series VARCHAR(10), voucher_number VARCHAR(50), voucher_url TEXT, -- 处理状态 status VARCHAR(20) DEFAULT 'pending', -- pending, processing, preview, imported, failed error_message TEXT, -- 用户操作 reviewed_by UUID, reviewed_at TIMESTAMP, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); -- 供应商缓存 CREATE TABLE supplier_cache ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES fortnox_tenants(id), supplier_number VARCHAR(50) NOT NULL, name VARCHAR(255), organisation_number VARCHAR(20), address TEXT, phone VARCHAR(50), email VARCHAR(255), cached_at TIMESTAMP DEFAULT NOW(), UNIQUE(tenant_id, supplier_number) ); -- 处理队列 CREATE TABLE processing_queue ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), invoice_id UUID NOT NULL REFERENCES fortnox_invoices(id), status VARCHAR(20) DEFAULT 'queued', -- queued, processing, completed, failed priority INTEGER DEFAULT 5, retry_count INTEGER DEFAULT 0, max_retries INTEGER DEFAULT 3, scheduled_at TIMESTAMP DEFAULT NOW(), started_at TIMESTAMP, completed_at TIMESTAMP, error_message TEXT ); -- 索引 CREATE INDEX idx_fortnox_invoices_tenant ON fortnox_invoices(tenant_id); CREATE INDEX idx_fortnox_invoices_status ON fortnox_invoices(status); CREATE INDEX idx_supplier_cache_tenant ON supplier_cache(tenant_id); CREATE INDEX idx_processing_queue_status ON processing_queue(status); ``` --- ## 安全设计 ### 10.1 认证安全 **Token存储:** - Access Token和Refresh Token使用AES-256加密存储 - 加密密钥存储在Azure Key Vault / AWS Secrets Manager - Token定期轮换 **OAuth安全:** - 使用state参数防止CSRF攻击 - 强制HTTPS回调 - 授权码一次性使用 ### 10.2 数据安全 **传输安全:** - 所有API通信强制TLS 1.3 - 证书固定(Certificate Pinning)防止中间人攻击 **存储安全:** - 发票PDF加密存储(AES-256) - 数据库连接使用SSL - 敏感字段加密(组织号、银行信息) ### 10.3 访问控制 ```python # 权限检查装饰器 async def require_fortnox_connection(func): @wraps(func) async def wrapper(request: Request, *args, **kwargs): tenant_id = request.headers.get('X-Tenant-ID') # 检查是否已连接Fortnox connection = await get_fortnox_connection(tenant_id) if not connection or not connection.is_active: raise HTTPException( status_code=401, detail="Fortnox connection required" ) # 检查Token是否有效 if connection.is_token_expired(): await refresh_fortnox_token(connection) return await func(request, *args, **kwargs) return wrapper ``` --- ## 错误处理 ### 11.1 错误分类 | 错误类型 | 示例 | 处理策略 | |---------|------|----------| | **认证错误** | Token过期、无效 | 自动刷新或提示重新授权 | | **API限制** | 429 Too Many Requests | 指数退避重试 | | **数据错误** | 无效的组织号格式 | 返回具体验证错误 | | **网络错误** | 连接超时 | 重试3次后失败 | | **业务错误** | 供应商不存在 | 提供创建选项 | ### 11.2 错误响应格式 ```json { "error": { "code": "FORTNOX_TOKEN_EXPIRED", "message": "Fortnox access token has expired", "details": { "action": "RECONNECT_REQUIRED", "reconnect_url": "/api/v1/fortnox/auth/url" }, "timestamp": "2024-01-15T10:30:00Z", "request_id": "req_123456" } } ``` ### 11.3 重试策略 ```python class FortnoxAPIRetry: """Fortnox API重试策略""" def __init__(self): self.max_retries = 3 self.base_delay = 1 # 秒 async def execute(self, func, *args, **kwargs): for attempt in range(self.max_retries): try: return await func(*args, **kwargs) except FortnoxAPIError as e: if e.status_code == 429: # Rate limit delay = self.base_delay * (2 ** attempt) await asyncio.sleep(delay) elif e.status_code in [500, 502, 503, 504]: if attempt < self.max_retries - 1: delay = self.base_delay * (2 ** attempt) await asyncio.sleep(delay) else: raise else: raise ``` --- ## 开发计划 ### 12.1 里程碑 | 阶段 | 时间 | 目标 | 交付物 | |------|------|------|--------| | **M1** | Week 1-2 | 基础架构 | 认证模块、数据库 | | **M2** | Week 3-4 | 核心功能 | 发票处理、供应商匹配 | | **M3** | Week 5-6 | Fortnox集成 | API集成、凭证创建 | | **M4** | Week 7-8 | UI开发 | 前端界面、用户流程 | | **M5** | Week 9-10 | 测试优化 | 测试、性能优化 | | **M6** | Week 11-12 | 上线准备 | 文档、审核、部署 | ### 12.2 任务分解 **Week 1-2: 基础架构** - [ ] 创建Fortnox开发者账号 - [ ] 设计数据库Schema - [ ] 实现OAuth2认证流程 - [ ] Token管理和刷新机制 - [ ] 基础API客户端 **Week 3-4: 核心功能** - [ ] 集成Invoice Master OCR - [ ] 实现供应商匹配算法 - [ ] 文件上传和存储 - [ ] 异步处理队列 **Week 5-6: Fortnox集成** - [ ] 供应商API集成 - [ ] 凭证创建逻辑 - [ ] 文件附件上传 - [ ] 错误处理和重试 **Week 7-8: UI开发** - [ ] 连接设置页面 - [ ] 发票上传界面 - [ ] 结果预览/编辑页面 - [ ] 历史记录页面 **Week 9-10: 测试优化** - [ ] 单元测试 (目标80%覆盖率) - [ ] 集成测试 - [ ] 性能测试 - [ ] 安全审计 **Week 11-12: 上线准备** - [ ] 用户文档 - [ ] API文档 - [ ] Fortnox审核申请 - [ ] 生产环境部署 --- ## 测试策略 ### 13.1 测试类型 | 测试类型 | 工具 | 覆盖率目标 | 说明 | |---------|------|-----------|------| | **单元测试** | pytest | 80% | 核心逻辑 | | **集成测试** | pytest + httpx | - | Fortnox API交互 | | **E2E测试** | Playwright | 核心流程 | 用户场景 | | **性能测试** | Locust | - | 并发处理 | | **安全测试** | bandit, safety | - | 漏洞扫描 | ### 13.2 测试用例示例 ```python # 供应商匹配测试 class TestSupplierMatcher: async def test_exact_org_number_match(self): """测试组织号精确匹配""" matcher = FortnoxSupplierMatcher(mock_client) result = await matcher.match( tenant_id="test", extraction=ExtractionResult( supplier_org_number="556677-8899" ) ) assert result.action == 'USE_EXISTING' assert result.confidence == 1.0 async def test_fuzzy_name_match(self): """测试名称模糊匹配""" result = await matcher.match( tenant_id="test", extraction=ExtractionResult( supplier_name="ABC Company AB" ) ) assert result.confidence > 0.85 # Fortnox API集成测试 class TestFortnoxIntegration: async def test_create_voucher(self): """测试创建会计凭证""" creator = FortnoxVoucherCreator(client) result = await creator.create_voucher( tenant_id="test", extraction=mock_extraction, supplier_number="123", settings=mock_settings ) assert result.voucher_id is not None ``` --- ## 部署方案 ### 14.1 架构部署图 ``` ┌─────────────────────────────────────────────────────────────┐ │ Azure │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Container │ │ PostgreSQL │ │ Blob │ │ │ │ Apps │ │ Flexible │ │ Storage │ │ │ │ (FastAPI) │ │ Server │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ │ └─────────────────┼─────────────────┘ │ │ │ │ │ ┌──────▼──────┐ │ │ │ Redis │ │ │ │ Cache │ │ │ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### 14.2 资源配置 **Container Apps:** - CPU: 1 vCPU - Memory: 2 GiB - Min replicas: 1 - Max replicas: 5 **PostgreSQL:** - SKU: Standard_B1ms - Storage: 32 GB - Backup: 7 days **Blob Storage:** - Tier: Hot - Redundancy: LRS ### 14.3 部署流程 ```bash # 1. 基础设施部署 az group create --name invoice-master-rg --location swedencentral # 2. 数据库部署 az postgres flexible-server create \ --name invoice-master-db \ --resource-group invoice-master-rg \ --sku-name Standard_B1ms # 3. 应用部署 az containerapp create \ --name invoice-master-fortnox \ --resource-group invoice-master-rg \ --image invoicemaster.azurecr.io/fortnox-integration:latest \ --cpu 1 --memory 2Gi \ --min-replicas 1 --max-replicas 5 ``` --- ## 附录 ### A. Fortnox API参考 **常用端点速查:** | 功能 | 方法 | 端点 | |------|------|------| | 获取公司信息 | GET | /3/companyinformation | | 获取供应商列表 | GET | /3/suppliers | | 创建供应商 | POST | /3/suppliers | | 获取会计科目 | GET | /3/accounts | | 创建凭证 | POST | /3/vouchers | | 上传文件 | POST | /3/inbox | ### B. 科目表参考 (BAS2024) **常用费用科目:** | 代码 | 名称 | 说明 | |------|------|------| | 2440 | Leverantörsskulder | 应付账款 | | 2610 | Ingående moms | 进项VAT 25% | | 2620 | Ingående moms | 进项VAT 12% | | 2630 | Ingående moms | 进项VAT 6% | | 5460 | Kontorsmaterial | 办公用品 | | 5710 | Frakter | 运输费 | | 6100 | Övriga externa tjänster | 其他外部服务 | | 6210 | Konsultarvoden | 咨询费 | ### C. 错误代码表 | 代码 | 说明 | HTTP状态 | |------|------|---------| | FORTNOX_TOKEN_EXPIRED | Token过期 | 401 | | FORTNOX_RATE_LIMITED | 请求过于频繁 | 429 | | SUPPLIER_NOT_FOUND | 供应商不存在 | 404 | | INVALID_ORG_NUMBER | 无效的组织号 | 400 | | EXTRACTION_FAILED | OCR提取失败 | 422 | ### D. 相关链接 - [Fortnox Developer Portal](https://developer.fortnox.se/) - [Fortnox API Docs](https://api.fortnox.se/apidocs) - [BAS Kontoplan](https://www.bas.se/) --- **文档版本历史:** | 版本 | 日期 | 作者 | 变更说明 | |------|------|------|---------| | 1.0 | 2026-02-01 | Claude Code | 初始版本,添加Fortnox集成模式说明和UI设计规范 | --- **审批:** - [ ] 技术负责人 - [ ] 产品经理 - [ ] 安全团队