docs: 更新 HTTP 错误码、用户积分、占卜运行及用户资料协议文档

This commit is contained in:
qzl
2026-04-10 16:45:45 +08:00
parent 1bc8bc6a27
commit 17ef460391
78 changed files with 18680 additions and 25 deletions
@@ -0,0 +1,533 @@
# Quality Guidelines
> Code quality standards for Flutter app development.
---
## Overview
This app enforces quality through:
- **Linting**: Flutter/Dart analysis
- **Architecture**: Feature-first with clear boundaries
- **Testing**: Widget tests, unit tests, integration tests
- **Code review**: Checklist-based reviews
---
## Forbidden Patterns
### ❌ Hardcoded Colors
```dart
// WRONG: Hardcoded hex color
Container(
color: Color(0xFF2196F3),
)
// WRONG: Using Colors.*
Container(
color: Colors.blue,
)
```
**Right: Use semantic colors:**
```dart
// Semantic colors from theme
Container(
color: Theme.of(context).colorScheme.primary,
)
// Brand palette colors
Container(
color: Theme.of(context).extension<AppColorPalette>()!.brandPrimary,
)
```
### ❌ Hardcoded Spacing
```dart
// WRONG: Magic numbers
Padding(
padding: EdgeInsets.all(16.0),
)
// WRONG: Hardcoded values
SizedBox(height: 24.0)
```
**Right: Use `AppSpacing` / `AppRadius`:**
```dart
import 'shared/theme/design_tokens.dart';
Padding(
padding: AppSpacing.allMedium,
)
SizedBox(height: AppSpacing.large)
```
### ❌ Feature Data Import from Other Features
```dart
// WRONG: Direct import from another feature
import 'package:app/features/auth/data/repositories/auth_repository.dart';
```
**Right: Access via app-level facade or DI:**
```dart
final authRepository = ServiceLocator.authRepository;
```
### ❌ Creating Per-Widget State Instances
```dart
// WRONG: New instance per build
class MyWidget extends StatelessWidget {
final authBloc = AuthBloc(); // New instance every time
}
```
**Right: Use singleton from DI:**
```dart
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final authBloc = ServiceLocator.authBloc; // Singleton
}
}
```
### ❌ Place Feature Repositories in Shared Data Layer
```dart
// WRONG: Feature repository in shared data/
// data/repositories/auth_repository.dart
class AuthRepositoryImpl implements AuthRepository {}
```
**Right: Keep in feature's data layer:**
```dart
// features/auth/data/repositories/auth_repository.dart
class AuthRepositoryImpl implements AuthRepository {}
```
### ❌ New Second-Level Directories Under `lib/`
```dart
// WRONG: Ad-hoc directories
lib/
utils/
helpers/
constants/
```
**Right: Use allowed directories only:**
```dart
lib/
app/ # Bootstrap, DI, router
core/ # Cross-feature infrastructure
data/ # Shared infrastructure
features/ # Feature modules
shared/ # Reusable widgets
l10n/ # Localization
```
---
## Required Patterns
### ✅ Semantic Color System
```dart
import 'package:flutter/material.dart';
// Use semantic colors
ThemeData lightTheme = ThemeData(
colorScheme: ColorScheme.light(
primary: Color(0xFF2196F3),
secondary: Color(0xFF03DAC6),
surface: Color(0xFFFFFFFF),
error: Color(0xFFB00020),
),
);
// Access via theme
Container(
color: Theme.of(context).colorScheme.primary,
)
```
### ✅ Design Tokens for Spacing
```dart
// shared/theme/design_tokens.dart
class AppSpacing {
static const double xsmall = 4.0;
static const double small = 8.0;
static const double medium = 16.0;
static const double large = 24.0;
static const double xlarge = 32.0;
static EdgeInsets get allMedium => EdgeInsets.all(medium);
static EdgeInsets get horizontalMedium => EdgeInsets.symmetric(horizontal: medium);
}
class AppRadius {
static const double small = 4.0;
static const double medium = 8.0;
static const double large = 12.0;
}
```
### ✅ Repository Pattern with DI
```dart
// app/di/di.dart
class ServiceLocator {
static late AuthRepository authRepository;
static late AuthBloc authBloc;
static void setup() {
authRepository = AuthRepositoryImpl(
api: AuthApiImpl(),
sessionStore: SessionStore(),
);
authBloc = AuthBloc(repository: authRepository);
}
}
```
### ✅ Immutable State with copyWith
```dart
class AuthState {
const AuthState({
required this.status,
this.user,
this.errorMessage,
});
final AuthStatus status;
final AuthUser? user;
final String? errorMessage;
AuthState copyWith({
AuthStatus? status,
AuthUser? user,
String? errorMessage,
}) {
return AuthState(
status: status ?? this.status,
user: user ?? this.user,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}
```
### ✅ Error Logging in Try-Catch
```dart
try {
await _repository.operation();
} catch (e, stackTrace) {
_logger.error(
message: 'Operation failed',
error: e,
stackTrace: stackTrace,
);
rethrow;
}
```
### ✅ Logger Module Naming
```dart
class AuthBloc extends ChangeNotifier {
final Logger _logger = getLogger('features.auth.bloc');
}
class HomeRepository {
final Logger _logger = getLogger('features.home.repository');
}
class HttpClient {
final Logger _logger = getLogger('core.network.http_client');
}
```
---
## Testing Requirements
### Test Organization
```
apps/test/
├── unit/ # Unit tests
├── widgets/ # Widget tests
├── integration/ # Integration tests
└── test_utils/ # Test utilities
```
### Test Requirements from AGENTS.md
1. **Prioritize tests for**: model parsing, service logic, high-regression flows
2. **Auth/Home/Cache changes**: must include targeted regression tests
3. **Simple static UI changes**: may skip tests
4. **Test credentials**: use environment config, never hardcode
### Unit Testing
```dart
test('AuthBloc login success', () async {
final mockRepo = MockAuthRepository();
final bloc = AuthBloc(repository: mockRepo);
when(mockRepo.loginWithEmailOtp(
email: 'test@example.com',
otp: '123456',
)).thenAnswer((_) async => AuthUser(id: '123', email: 'test@example.com'));
await bloc.loginWithOtp(email: 'test@example.com', otp: '123456');
expect(bloc.state.status, AuthStatus.authenticated);
expect(bloc.state.user?.id, '123');
});
```
### Widget Testing
```dart
testWidgets('Login screen shows error on failed login', (tester) async {
await tester.pumpWidget(MyApp());
await tester.enterText(find.byKey(Key('email_field')), 'test@example.com');
await tester.enterText(find.byKey(Key('otp_field')), 'wrong');
await tester.tap(find.byKey(Key('login_button')));
await tester.pumpAndSettle();
expect(find.text('Request failed, please try again'), findsOneWidget);
});
```
### Integration Testing
```dart
testWidgets('User can login with OTP', (tester) async {
await tester.pumpWidget(MyApp());
// Navigate to login
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
// Enter credentials
await tester.enterText(find.byKey(Key('email_field')), 'user@example.com');
await tester.tap(find.byKey(Key('send_otp_button')));
await tester.pumpAndSettle();
// Enter OTP
await tester.enterText(find.byKey(Key('otp_field')), '123456');
await tester.tap(find.byKey(Key('login_button')));
await tester.pumpAndSettle();
// Verify success
expect(find.text('Welcome'), findsOneWidget);
});
```
---
## Code Review Checklist
### Architecture
- [ ] Follows `features/<feature>/data/` and `features/<feature>/presentation/` structure
- [ ] Shared infrastructure in `data/` (not feature repositories/models)
- [ ] Reusable widgets in `shared/widgets/` (not feature-specific)
- [ ] Cross-feature code in `core/`
- [ ] No new second-level directories under `lib/`
### State Management
- [ ] Uses `ChangeNotifier` + immutable state
- [ ] State classes have `copyWith` method
- [ ] Singleton blocs via DI (not per-widget instances)
- [ ] Errors are logged before state changes
### UI
- [ ] Uses semantic colors from `Theme.of(context).colorScheme`
- [ ] Uses `AppSpacing` / `AppRadius` (no hardcoded values)
- [ ] Follows design system tokens
- [ ] Toast/Banner for user feedback (no `print()`)
### Data
- [ ] Repository pattern for data access
- [ ] `ApiProblem` for error handling
- [ ] Error codes mapped to l10n keys
- [ ] 401 handled via global callback
### Logging
- [ ] Logger initialized with module path (`features.<feature>.<component>`)
- [ ] All exceptions logged with `error`, `stackTrace`, and `message`
- [ ] No PII/tokens/passwords in logs
- [ ] Info logs only for milestones (login, logout, run completed)
### Testing
- [ ] Unit tests for bloc logic
- [ ] Widget tests for UI
- [ ] Integration tests for critical flows
- [ ] Mocks use `when/thenAnswer/thenReturn`
---
## High-Risk Modules Checklist
From AGENTS.md, these modules require extra attention:
### Auth
- [ ] `AuthBloc` is single source of truth
- [ ] 401 invalidation goes through global callback
- [ ] No feature-level token clearing or direct login navigation
### Home Message Viewport
- [ ] Auto-scroll/anchor restore is event-driven
- [ ] Viewport preserved during history prepend
- [ ] User reading position maintained
### Cache / Repository
- [ ] Reads/writes go through repository layer
- [ ] Cache keys/invalidation in repository (not UI/Bloc)
- [ ] Feature-scoped TTL policy defined in repository
- [ ] No per-screen/per-widget cache store instances
- [ ] Cross-feature access via app-level facade
---
## Common Mistakes
### ❌ Mutable State
```dart
// WRONG: Direct mutation
class AuthBloc extends ChangeNotifier {
AuthUser? user; // Mutable
void login() {
user = fetchUser(); // Direct mutation
notifyListeners();
}
}
```
**Right: Immutable state:**
```dart
class AuthBloc extends ChangeNotifier {
AuthState _state = AuthState.initial;
AuthState get state => _state;
void login() async {
final user = await fetchUser();
_state = _state.copyWith(user: user, status: AuthStatus.authenticated);
notifyListeners();
}
}
```
### ❌ Missing Error Logging
```dart
// WRONG: No logging
try {
await operation();
} catch (e) {
state = ErrorState();
}
```
**Right: Log before state change:**
```dart
try {
await operation();
} catch (e, stackTrace) {
_logger.error(message: 'Operation failed', error: e, stackTrace: stackTrace);
state = ErrorState();
}
```
### ❌ Hardcoded Strings
```dart
// WRONG: User-facing string
Text('Login Failed')
```
**Right: Use l10n:**
```dart
Text(AppLocalizations.of(context)!.loginFailed)
```
### ❌ Bypassing Design System
```dart
// WRONG: Magic numbers
Padding(
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
)
```
**Right: Use tokens:**
```dart
Padding(
padding: EdgeInsets.symmetric(
horizontal: AppSpacing.medium,
vertical: AppSpacing.small,
),
)
```
---
## Flutter-Specific Quality
### Widget Best Practices
- Use `const` constructors for immutable widgets
- Prefer `StatelessWidget` over `StatefulWidget` when possible
- Use `const` when creating widgets in build methods
- Avoid `print()` - use Logger instead
### Performance
- Use `ListView.builder` for long lists
- Avoid rebuilding expensive widgets unnecessarily
- Use `const` widgets to prevent rebuilds
- Use `RepaintBoundary` for complex animations
### Code Style
- Follow Dart style guide
- Use `flutter analyze` to catch issues
- Run `dart format .` before commits
- Follow naming conventions: `lowerCamelCase` for variables, `UpperCamelCase` for types