本设计文档描述工作统计系统的增强功能实现方案,包括:基于JWT的用户认证、管理员管理、工作记录按月筛选、前端北京时间显示、以及仪表盘月报功能。这些功能将在现有Flask后端和React前端基础上进行扩展。
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
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/
├── 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
| 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 |
| 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 |
| 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 |
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"
}
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
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
}
{
"header": {
"alg": "HS256",
"typ": "JWT"
},
"payload": {
"admin_id": 1,
"username": "admin",
"exp": 1735689600,
"iat": 1735084800
}
}
# config.py
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY', 'dev-secret-key')
JWT_EXPIRATION_DAYS = 7
JWT_ALGORITHM = 'HS256'
# 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
// 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>
);
}
// components/ProtectedRoute.jsx
function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return children;
}
// 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);
}
// Example usage in table columns
const columns = [
{
title: '创建时间',
dataIndex: 'created_at',
render: (text) => toBeijingDateTime(text)
},
{
title: '工作日期',
dataIndex: 'work_date',
render: (text) => toBeijingDate(text)
}
];
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# 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()
// WorkRecordList.jsx
<DatePicker.MonthPicker
placeholder="选择月份"
onChange={(date) => {
if (date) {
setYear(date.year());
setMonth(date.month() + 1);
} else {
setYear(null);
setMonth(null);
}
}}
/>
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.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>
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.
For any valid admin credentials (username, password), logging in should return a JWT token that:
Validates: Requirements 1.2, 1.8, 1.10
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
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
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
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
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
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
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
For any UTC datetime value, converting to Beijing time should add 8 hours, and the formatted string should match:
yyyy-MM-dd HH:mm:ss patternyyyy-MM-dd patternValidates: Requirements 4.1, 4.2, 4.4
For any month with work records:
Validates: Requirements 5.2, 5.3, 5.4, 5.5
{
"success": False,
"error": "Human-readable error message",
"code": "ERROR_CODE", # AUTH_ERROR, VALIDATION_ERROR, NOT_FOUND, etc.
"details": {} # Optional additional details
}
Unit tests focus on specific examples and edge cases:
Property-based tests verify universal properties across many generated inputs:
# 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}'}