design.md 17 KB

Design Document: System Enhancements

Overview

本设计文档描述工作统计系统的增强功能实现方案,包括:基于JWT的用户认证、管理员管理、工作记录按月筛选、前端北京时间显示、以及仪表盘月报功能。这些功能将在现有Flask后端和React前端基础上进行扩展。

Architecture

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/<id> 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:

{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIs...",
    "admin": {
      "id": 1,
      "username": "admin"
    }
  }
}

Login error response:

{
  "success": false,
  "error": "Invalid username or password",
  "code": "AUTH_ERROR"
}

Unauthorized response (HTTP 401):

{
  "success": false,
  "error": "Authentication required",
  "code": "UNAUTHORIZED"
}

Data Models

Admin Model (New)

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)

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

{
  "header": {
    "alg": "HS256",
    "typ": "JWT"
  },
  "payload": {
    "admin_id": 1,
    "username": "admin",
    "exp": 1735689600,
    "iat": 1735084800
  }
}

JWT Configuration

# config.py
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY', 'dev-secret-key')
JWT_EXPIRATION_DAYS = 7
JWT_ALGORITHM = 'HS256'

Auth Decorator

# 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

// 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 (
    <AuthContext.Provider value={{ token, admin, login, logout, isAuthenticated: !!token }}>
      {children}
    </AuthContext.Provider>
  );
}

Protected Route

// components/ProtectedRoute.jsx
function ProtectedRoute({ children }) {
  const { isAuthenticated } = useAuth();
  
  if (!isAuthenticated) {
    return <Navigate to="/login" />;
  }
  
  return children;
}

Beijing Time Display Design

Time Utility Functions

// 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

// 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

# 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

// WorkRecordList.jsx
<DatePicker.MonthPicker
  placeholder="选择月份"
  onChange={(date) => {
    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:

{
  "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

// Dashboard.jsx - Monthly Report Section
<Card title="月度报告">
  <DatePicker.MonthPicker 
    value={selectedMonth}
    onChange={setSelectedMonth}
  />
  <Row gutter={16}>
    <Col span={6}>
      <Statistic title="本月记录数" value={monthlyData.total_records} />
    </Col>
    <Col span={6}>
      <Statistic title="本月总收入" value={monthlyData.total_earnings} prefix="¥" />
    </Col>
  </Row>
  <Table 
    title="业绩排名"
    dataSource={monthlyData.top_performers}
    columns={[
      { title: '人员', dataIndex: 'person_name' },
      { title: '收入', dataIndex: 'earnings' }
    ]}
  />
</Card>

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

{
    "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

# 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