# Design Document: System Enhancements ## Overview 本设计文档描述工作统计系统的增强功能实现方案,包括:基于JWT的用户认证、管理员管理、工作记录按月筛选、前端北京时间显示、以及仪表盘月报功能。这些功能将在现有Flask后端和React前端基础上进行扩展。 ## Architecture ```mermaid graph TB subgraph Frontend React[React SPA] AuthContext[Auth Context] TimeUtils[Time Utils] end subgraph Backend API[Flask API Server] AuthMiddleware[JWT Auth Middleware] Services[Business Services] Models[SQLAlchemy Models] end subgraph Database PG[(PostgreSQL/SQLite)] end React -->|JWT in Header| API React --> AuthContext React --> TimeUtils API --> AuthMiddleware AuthMiddleware --> Services Services --> Models Models --> PG ``` ### Technology Stack Additions - **Authentication**: PyJWT (JWT token generation/validation) - **Password Hashing**: bcrypt (via Flask-Bcrypt) - **Frontend State**: React Context API (for auth state) - **Date/Time**: dayjs (for timezone conversion and formatting) ## Components and Interfaces ### Backend Structure (New/Modified Files) ``` backend/ ├── app/ │ ├── models/ │ │ └── admin.py # NEW: Admin model │ ├── routes/ │ │ ├── auth.py # NEW: Auth routes (login) │ │ └── admin.py # NEW: Admin management routes │ ├── services/ │ │ ├── auth_service.py # NEW: JWT generation/validation │ │ └── admin_service.py # NEW: Admin CRUD operations │ ├── utils/ │ │ └── auth_decorator.py # NEW: @require_auth decorator │ └── config.py # MODIFIED: Add JWT settings ``` ### Frontend Structure (New/Modified Files) ``` frontend/ ├── src/ │ ├── components/ │ │ ├── Login.jsx # NEW: Login page │ │ ├── AdminList.jsx # NEW: Admin management page │ │ ├── AdminForm.jsx # NEW: Admin add/edit form │ │ ├── WorkRecordList.jsx # MODIFIED: Add month filter │ │ └── Dashboard.jsx # MODIFIED: Add monthly report │ ├── contexts/ │ │ └── AuthContext.jsx # NEW: Auth state management │ ├── utils/ │ │ └── timeUtils.js # NEW: Beijing time conversion │ ├── services/ │ │ └── api.js # MODIFIED: Add JWT header │ └── App.jsx # MODIFIED: Add auth routing ``` ### API Endpoints #### Auth API (New) | Method | Endpoint | Description | Auth Required | |--------|----------|-------------|---------------| | POST | `/api/auth/login` | Login with username/password | No | | POST | `/api/auth/logout` | Logout (optional server-side) | Yes | | GET | `/api/auth/me` | Get current admin info | Yes | #### Admin API (New) | Method | Endpoint | Description | Auth Required | |--------|----------|-------------|---------------| | GET | `/api/admins` | List all admins | Yes | | GET | `/api/admins/` | Get admin by ID | Yes | | POST | `/api/admins/create` | Create new admin | Yes | | POST | `/api/admins/update` | Update admin | Yes | | POST | `/api/admins/delete` | Delete admin | Yes | #### Work Record API (Modified) | Method | Endpoint | Description | |--------|----------|-------------| | GET | `/api/work-records?year=&month=&person_id=` | List with month filter | | GET | `/api/work-records/monthly-summary?year=&month=` | Monthly summary for dashboard | ### Response Format Login success response: ```json { "success": true, "data": { "token": "eyJhbGciOiJIUzI1NiIs...", "admin": { "id": 1, "username": "admin" } } } ``` Login error response: ```json { "success": false, "error": "Invalid username or password", "code": "AUTH_ERROR" } ``` Unauthorized response (HTTP 401): ```json { "success": false, "error": "Authentication required", "code": "UNAUTHORIZED" } ``` ## Data Models ### Admin Model (New) ```python class Admin: id: int (primary key, auto-increment) username: str (required, unique, non-empty) password_hash: str (required, bcrypt hashed) created_at: datetime updated_at: datetime ``` ### Entity Relationship Diagram (Updated) ```mermaid erDiagram Admin { int id PK string username UK string password_hash datetime created_at datetime updated_at } Person ||--o{ WorkRecord : has Item ||--o{ WorkRecord : has Person { int id PK string name datetime created_at datetime updated_at } Item { int id PK string name decimal unit_price datetime created_at datetime updated_at } WorkRecord { int id PK int person_id FK int item_id FK date work_date int quantity datetime created_at datetime updated_at } ``` ## JWT Authentication Design ### JWT Token Structure ```json { "header": { "alg": "HS256", "typ": "JWT" }, "payload": { "admin_id": 1, "username": "admin", "exp": 1735689600, "iat": 1735084800 } } ``` ### JWT Configuration ```python # config.py JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY', 'dev-secret-key') JWT_EXPIRATION_DAYS = 7 JWT_ALGORITHM = 'HS256' ``` ### Auth Decorator ```python # utils/auth_decorator.py from functools import wraps from flask import request, jsonify import jwt def require_auth(f): @wraps(f) def decorated(*args, **kwargs): token = request.headers.get('Authorization', '').replace('Bearer ', '') if not token: return jsonify({ 'success': False, 'error': 'Authentication required', 'code': 'UNAUTHORIZED' }), 401 try: payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) request.current_admin = payload except jwt.ExpiredSignatureError: return jsonify({ 'success': False, 'error': 'Token expired', 'code': 'TOKEN_EXPIRED' }), 401 except jwt.InvalidTokenError: return jsonify({ 'success': False, 'error': 'Invalid token', 'code': 'INVALID_TOKEN' }), 401 return f(*args, **kwargs) return decorated ``` ## Frontend Auth Design ### Auth Context ```jsx // contexts/AuthContext.jsx const AuthContext = createContext(); export function AuthProvider({ children }) { const [token, setToken] = useState(localStorage.getItem('token')); const [admin, setAdmin] = useState(null); const login = async (username, password) => { const response = await api.post('/auth/login', { username, password }); if (response.success) { localStorage.setItem('token', response.data.token); setToken(response.data.token); setAdmin(response.data.admin); } return response; }; const logout = () => { localStorage.removeItem('token'); setToken(null); setAdmin(null); }; return ( {children} ); } ``` ### Protected Route ```jsx // components/ProtectedRoute.jsx function ProtectedRoute({ children }) { const { isAuthenticated } = useAuth(); if (!isAuthenticated) { return ; } return children; } ``` ## Beijing Time Display Design ### Time Utility Functions ```javascript // utils/timeUtils.js import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; dayjs.extend(utc); dayjs.extend(timezone); const BEIJING_TIMEZONE = 'Asia/Shanghai'; const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; const DATE_FORMAT = 'YYYY-MM-DD'; // Convert UTC datetime to Beijing time string export function toBeijingDateTime(utcDatetime) { if (!utcDatetime) return ''; return dayjs.utc(utcDatetime).tz(BEIJING_TIMEZONE).format(DATETIME_FORMAT); } // Convert UTC date to Beijing date string export function toBeijingDate(utcDate) { if (!utcDate) return ''; return dayjs.utc(utcDate).tz(BEIJING_TIMEZONE).format(DATE_FORMAT); } ``` ### Usage in Components ```jsx // Example usage in table columns const columns = [ { title: '创建时间', dataIndex: 'created_at', render: (text) => toBeijingDateTime(text) }, { title: '工作日期', dataIndex: 'work_date', render: (text) => toBeijingDate(text) } ]; ``` ## Monthly Filter Design ### Work Record Filter API ``` GET /api/work-records?year=2024&month=12&person_id=1 ``` Query parameters: - `year` (optional): Filter by year (e.g., 2024) - `month` (optional): Filter by month (1-12) - `person_id` (optional): Filter by person ### Backend Filter Logic ```python # services/work_record_service.py def get_work_records(year=None, month=None, person_id=None): query = WorkRecord.query if year and month: start_date = date(year, month, 1) if month == 12: end_date = date(year + 1, 1, 1) else: end_date = date(year, month + 1, 1) query = query.filter(WorkRecord.work_date >= start_date) query = query.filter(WorkRecord.work_date < end_date) if person_id: query = query.filter(WorkRecord.person_id == person_id) return query.all() ``` ### Frontend Month Picker ```jsx // WorkRecordList.jsx { if (date) { setYear(date.year()); setMonth(date.month() + 1); } else { setYear(null); setMonth(null); } }} /> ``` ## Dashboard Monthly Report Design ### Monthly Summary API ``` GET /api/work-records/monthly-summary?year=2024&month=12 ``` Response: ```json { "success": true, "data": { "year": 2024, "month": 12, "total_records": 150, "total_earnings": 25000.00, "top_performers": [ {"person_id": 1, "person_name": "张三", "earnings": 8000.00}, {"person_id": 2, "person_name": "李四", "earnings": 7500.00} ], "item_breakdown": [ {"item_id": 1, "item_name": "物品A", "quantity": 100, "earnings": 10000.00}, {"item_id": 2, "item_name": "物品B", "quantity": 80, "earnings": 8000.00} ] } } ``` ### Dashboard Monthly Report Component ```jsx // Dashboard.jsx - Monthly Report Section ``` ## 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: JWT Authentication Round-Trip *For any* valid admin credentials (username, password), logging in should return a JWT token that: - Contains the correct admin_id and username in the payload - Has an expiration time exactly 7 days from issuance - Can be decoded and verified with the secret key **Validates: Requirements 1.2, 1.8, 1.10** ### Property 2: Invalid Credentials Rejection *For any* invalid credentials (non-existent username OR wrong password), the login attempt should be rejected with an authentication error and no token should be returned. **Validates: Requirements 1.3** ### Property 3: Protected Endpoint Authentication *For any* protected API endpoint and any request without a valid JWT token (missing, expired, or malformed), the system should return HTTP 401 Unauthorized. **Validates: Requirements 1.5, 1.6, 1.7** ### Property 4: Admin CRUD Round-Trip *For any* valid admin data (unique username, password >= 6 chars), creating an admin, then retrieving it, should return the same username. Updating the admin and retrieving again should return the updated values. Deleting the admin (when not the last one) should make it no longer retrievable. The admin list should never expose password_hash. **Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5** ### Property 5: Admin Username Uniqueness *For any* existing admin username, attempting to create another admin with the same username should be rejected with a validation error. **Validates: Requirements 2.6** ### Property 6: Admin Password Validation *For any* password with length less than 6 characters, attempting to create or update an admin should be rejected with a validation error. **Validates: Requirements 2.7** ### Property 7: Password Secure Hashing *For any* admin password, the stored password_hash should not equal the plaintext password, and bcrypt.checkpw should return True when verifying the original password against the hash. **Validates: Requirements 2.9** ### Property 8: Work Record Month Filter Consistency *For any* set of work records and a month filter (year, month), the filtered results should contain only records where work_date falls within that month. When combined with person_id filter, results should match both criteria. **Validates: Requirements 3.1, 3.2, 3.5** ### Property 9: DateTime Beijing Time Formatting *For any* UTC datetime value, converting to Beijing time should add 8 hours, and the formatted string should match: - For datetime: `yyyy-MM-dd HH:mm:ss` pattern - For date only: `yyyy-MM-dd` pattern **Validates: Requirements 4.1, 4.2, 4.4** ### Property 10: Monthly Summary Consistency *For any* month with work records: - total_records should equal the count of records in that month - total_earnings should equal the sum of all record total_prices - top_performers should be correctly ranked by earnings (descending) - item_breakdown totals should sum to total_earnings **Validates: Requirements 5.2, 5.3, 5.4, 5.5** ## Error Handling ### Authentication Errors (HTTP 401) - Missing Authorization header - Invalid JWT token format - Expired JWT token - Invalid JWT signature - Invalid credentials (wrong username/password) ### Validation Errors (HTTP 400) - Empty or whitespace-only username - Password less than 6 characters - Duplicate username - Invalid year/month parameters ### Business Logic Errors (HTTP 400) - Attempting to delete the last admin account ### Not Found Errors (HTTP 404) - Admin not found by ID ### Error Response Format ```python { "success": False, "error": "Human-readable error message", "code": "ERROR_CODE", # AUTH_ERROR, VALIDATION_ERROR, NOT_FOUND, etc. "details": {} # Optional additional details } ``` ## Testing Strategy ### Testing Framework - **Unit Tests**: pytest - **Property-Based Tests**: hypothesis (Python PBT library) - **Frontend Tests**: Jest + React Testing Library ### Unit Tests Unit tests focus on specific examples and edge cases: - Test login with valid/invalid credentials - Test JWT token generation and validation - Test admin CRUD operations - Test month filter with various date ranges - Test Beijing time conversion edge cases (midnight, year boundaries) - Test monthly summary calculations ### Property-Based Tests Property-based tests verify universal properties across many generated inputs: - **Minimum 100 iterations** per property test - Each test references its design document property - Tag format: **Feature: system-enhancements, Property N: [property description]** ### Test Configuration ```python # conftest.py additions import pytest from hypothesis import settings # Configure hypothesis for minimum 100 examples settings.register_profile("ci", max_examples=100) settings.load_profile("ci") @pytest.fixture def auth_token(client, test_admin): """Get a valid JWT token for testing protected endpoints.""" response = client.post('/api/auth/login', json={ 'username': test_admin.username, 'password': 'testpassword' }) return response.json['data']['token'] @pytest.fixture def auth_headers(auth_token): """Get headers with JWT token for authenticated requests.""" return {'Authorization': f'Bearer {auth_token}'} ``` ### Test Coverage Goals - JWT token generation and validation - All admin CRUD operations - Authentication decorator on all protected endpoints - Month filter logic with edge cases (month boundaries, leap years) - Beijing time conversion accuracy - Monthly summary aggregation correctness