# Design Document: Supplier Management ## Overview 本设计文档描述供应商管理功能的技术实现方案,包括: - 人员和物品名称唯一性约束 - 供应商管理模块(CRUD) - 物品关联供应商 - 工作记录结算状态管理 - 导出报表增加供应商信息 - 仪表盘供应商相关统计 - 数据库迁移脚本 ## Architecture ### 系统架构 ```mermaid graph TB subgraph Frontend Dashboard[Dashboard.jsx] SupplierMgmt[SupplierManagement.jsx] ItemMgmt[ItemManagement.jsx] WorkRecordMgmt[WorkRecordManagement.jsx] end subgraph Backend API SupplierRoutes[/api/suppliers] PersonRoutes[/api/persons] ItemRoutes[/api/items] WorkRecordRoutes[/api/work-records] ExportRoutes[/api/export] end subgraph Services ExportService[ExportService] end subgraph Models Supplier[Supplier Model] Person[Person Model] Item[Item Model] WorkRecord[WorkRecord Model] end subgraph Database DB[(SQLite/PostgreSQL)] end Dashboard --> WorkRecordRoutes SupplierMgmt --> SupplierRoutes ItemMgmt --> ItemRoutes WorkRecordMgmt --> WorkRecordRoutes SupplierRoutes --> Supplier PersonRoutes --> Person ItemRoutes --> Item WorkRecordRoutes --> WorkRecord ExportRoutes --> ExportService ExportService --> WorkRecord ExportService --> Item ExportService --> Supplier Supplier --> DB Person --> DB Item --> DB WorkRecord --> DB ``` ## Components and Interfaces ### 1. Supplier Model (Backend) ```python class Supplier(db.Model): __tablename__ = 'suppliers' id = db.Column(db.Integer, primary_key=True, autoincrement=True) name = db.Column(db.String(100), nullable=False, unique=True, index=True) created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) def to_dict(self): return { 'id': self.id, 'name': self.name, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None } ``` ### 2. Updated Item Model ```python class Item(db.Model): __tablename__ = 'items' id = db.Column(db.Integer, primary_key=True, autoincrement=True) name = db.Column(db.String(100), nullable=False, unique=True, index=True) unit_price = db.Column(db.Float, nullable=False) supplier_id = db.Column(db.Integer, db.ForeignKey('suppliers.id'), nullable=True) created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) supplier = db.relationship('Supplier', backref=db.backref('items', lazy='dynamic')) def to_dict(self): return { 'id': self.id, 'name': self.name, 'unit_price': self.unit_price, 'supplier_id': self.supplier_id, 'supplier_name': self.supplier.name if self.supplier else '', 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None } ``` ### 3. Updated Person Model ```python class Person(db.Model): __tablename__ = 'persons' id = db.Column(db.Integer, primary_key=True, autoincrement=True) name = db.Column(db.String(100), nullable=False, unique=True, index=True) # ... rest unchanged ``` ### 4. Updated WorkRecord Model ```python class WorkRecord(db.Model): __tablename__ = 'work_records' # ... existing fields ... is_settled = db.Column(db.Boolean, nullable=False, default=False) def to_dict(self): return { # ... existing fields ... 'is_settled': self.is_settled, 'supplier_id': self.item.supplier_id if self.item else None, 'supplier_name': self.item.supplier.name if self.item and self.item.supplier else '' } ``` ### 5. Supplier API Routes | Method | Endpoint | Description | |--------|----------|-------------| | GET | /api/suppliers | 获取所有供应商列表 | | GET | /api/suppliers/:id | 获取单个供应商详情 | | POST | /api/suppliers | 创建新供应商 | | PUT | /api/suppliers/:id | 更新供应商信息 | | DELETE | /api/suppliers/:id | 删除供应商 | ### 6. Updated Work Record API Routes | Method | Endpoint | Description | |--------|----------|-------------| | PUT | /api/work-records/:id/settlement | 切换单条记录结算状态 | | POST | /api/work-records/batch-settlement | 批量更新结算状态 | **批量更新请求体:** ```json { "person_id": 1, // 可选 "year": 2024, // 必填 "month": 12, // 必填 "supplier_id": 1, // 可选 "is_settled": true // 必填 } ``` ### 7. Frontend Components #### SupplierManagement.jsx - 供应商列表表格 - 新增/编辑供应商模态框 - 删除确认对话框 #### Updated ItemManagement.jsx - 物品表单增加供应商下拉选择 - 物品列表增加供应商列 #### Updated WorkRecordManagement.jsx - 工作记录列表增加结算状态列 - 单条记录结算状态切换按钮 - 批量操作模态框(人员、月份、供应商筛选) #### Updated Dashboard.jsx - 月度报告物品明细增加供应商列 - 新增人员按供应商收入明细表格 - 工作统计详情增加供应商列 - 系统统计显示供应商数量 ## Data Models ### Entity Relationship Diagram ```mermaid erDiagram SUPPLIER { int id PK string name UK datetime created_at datetime updated_at } PERSON { int id PK string name UK datetime created_at datetime updated_at } ITEM { int id PK string name UK float unit_price int supplier_id FK datetime created_at datetime updated_at } WORK_RECORD { int id PK int person_id FK int item_id FK date work_date int quantity boolean is_settled datetime created_at datetime updated_at } SUPPLIER ||--o{ ITEM : "supplies" PERSON ||--o{ WORK_RECORD : "creates" ITEM ||--o{ WORK_RECORD : "used_in" ``` ### Database Migration Script 迁移脚本需要兼容 SQLite 和 PostgreSQL: ```python def upgrade(): # 1. 创建 suppliers 表 op.create_table('suppliers', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('name', sa.String(100), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id') ) op.create_index('ix_suppliers_name', 'suppliers', ['name'], unique=True) # 2. 为 persons 表添加唯一约束 op.create_index('ix_persons_name_unique', 'persons', ['name'], unique=True) # 3. 为 items 表添加唯一约束和 supplier_id op.create_index('ix_items_name_unique', 'items', ['name'], unique=True) op.add_column('items', sa.Column('supplier_id', sa.Integer(), nullable=True)) op.create_foreign_key('fk_items_supplier', 'items', 'suppliers', ['supplier_id'], ['id']) # 4. 为 work_records 表添加 is_settled op.add_column('work_records', sa.Column('is_settled', sa.Boolean(), nullable=False, server_default='0')) def downgrade(): op.drop_column('work_records', 'is_settled') op.drop_constraint('fk_items_supplier', 'items', type_='foreignkey') op.drop_column('items', 'supplier_id') op.drop_index('ix_items_name_unique', 'items') op.drop_index('ix_persons_name_unique', 'persons') op.drop_index('ix_suppliers_name', 'suppliers') op.drop_table('suppliers') ``` ## Export Service Updates ### Updated Detail Sheet Headers ```python DETAIL_HEADERS = ['人员', '日期', '供应商', '物品', '单价', '数量', '总价'] ``` ### Updated Monthly Summary Sheet 月度汇总按人员和供应商分组: ```python # Headers: ['人员', '供应商', '总金额'] # 按 (person_name, supplier_name) 分组统计 ``` ### Yearly Summary Sheet 保持现有格式不变。 ## Dashboard API Updates ### Monthly Summary Response ```json { "total_records": 100, "total_earnings": 5000.00, "top_performers": [...], "item_breakdown": [ { "item_id": 1, "item_name": "物品A", "supplier_name": "供应商X", "quantity": 50, "earnings": 1000.00 } ], "supplier_breakdown": [ { "person_id": 1, "person_name": "张三", "supplier_name": "供应商X", "earnings": 500.00 } ] } ``` ### Daily Summary Response ```json { "summary": [ { "person_id": 1, "person_name": "张三", "supplier_name": "供应商X", "total_items": 10, "total_value": 200.00 } ], "grand_total_items": 100, "grand_total_value": 2000.00 } ``` ## Correctness Properties *A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* ### Property 1: Person Name Uniqueness *For any* two persons in the system, their names must be different. Creating or updating a person with an existing name should fail with an error. **Validates: Requirements 1.1, 1.2, 1.3** ### Property 2: Item Name Uniqueness *For any* two items in the system, their names must be different. Creating or updating an item with an existing name should fail with an error. **Validates: Requirements 2.1, 2.2, 2.3** ### Property 3: Supplier Name Uniqueness *For any* two suppliers in the system, their names must be different. Creating or updating a supplier with an existing name should fail with an error. **Validates: Requirements 3.2, 3.3, 3.6** ### Property 4: Empty Supplier Name Display *For any* item without a supplier (supplier_id is null), the supplier_name field in API responses, export reports, and dashboard displays should be an empty string. **Validates: Requirements 4.2, 6.4, 7.3, 8.2** ### Property 5: Work Record Settlement Default *For any* newly created work record, the is_settled field should default to False. **Validates: Requirements 5.1** ### Property 6: Settlement Status Toggle *For any* work record, calling the settlement toggle API should flip the is_settled value (True becomes False, False becomes True). **Validates: Requirements 5.3** ### Property 7: Batch Settlement Filter Intersection *For any* batch settlement request with person_id, year/month, and supplier_id filters, only work records matching ALL specified criteria should be updated. **Validates: Requirements 5.5, 5.7** ## Error Handling ### API Error Responses | Error Code | Description | HTTP Status | |------------|-------------|-------------| | DUPLICATE_NAME | 名称已存在 | 400 | | NOT_FOUND | 资源不存在 | 404 | | INVALID_INPUT | 输入参数无效 | 400 | | FOREIGN_KEY_VIOLATION | 外键约束违反(如删除有关联物品的供应商) | 400 | ### Error Response Format ```json { "error": "DUPLICATE_NAME", "message": "供应商名称已存在" } ``` ### Constraint Violation Handling - 删除供应商时,如果有物品关联该供应商,返回错误提示 - 或者可以选择将关联物品的 supplier_id 设为 null ## Testing Strategy ### Unit Tests - Supplier CRUD operations - Person/Item name uniqueness validation - WorkRecord settlement status operations - Export service with supplier data - Dashboard API with supplier statistics ### Property-Based Tests 使用 `hypothesis` 库进行属性测试: 1. **Name Uniqueness Property Test** - 生成随机名称,验证重复创建失败 - 最少 100 次迭代 2. **Empty Supplier Display Property Test** - 生成无供应商的物品和工作记录 - 验证所有输出中 supplier_name 为空字符串 3. **Settlement Toggle Property Test** - 生成随机工作记录 - 验证切换操作的幂等性(切换两次回到原状态) 4. **Batch Settlement Filter Property Test** - 生成随机工作记录集合 - 验证批量更新只影响符合所有条件的记录 ### Integration Tests - 完整的供应商管理流程 - 物品关联供应商流程 - 工作记录批量结算流程 - 导出报表包含供应商数据 ### Test Configuration ```python # conftest.py from hypothesis import settings settings.register_profile("ci", max_examples=100) settings.load_profile("ci") ```