create-dwy 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +1 -1
- package/package.json +2 -2
- package/src/create.js +61 -37
- package/src/sync.js +213 -53
- package/templates/claude-global/CLAUDE.md +92 -0
- package/templates/claude-global/rules/dwy-android-core.md +610 -0
- package/templates/claude-global/rules/dwy-api-design.md +110 -0
- package/templates/claude-global/rules/dwy-db-migration.md +46 -0
- package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
- package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
- package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
- package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
- package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
- package/templates/claude-global/rules/dwy-git-security.md +53 -0
- package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
- package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
- package/templates/claude-global/rules/dwy-project-structure.md +181 -0
- package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +314 -357
- package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
- package/templates/claude-global/rules/dwy-python-orm.md +61 -0
- package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +116 -79
- package/templates/claude-global/rules/dwy-python-testing.md +53 -0
- package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
- package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
- package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
- package/templates/claude-global/rules/dwy-vue-api.md +146 -0
- package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +361 -152
- package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
- package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
- package/templates/claude-global/settings.json +20 -0
- package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
- package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
- package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
- package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
- package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
- package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
- package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
- package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
- package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
- package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
- package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
- package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
- package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
- package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
- package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
- package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
- package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
- package/templates/claude-global/skills/dwy-publish/SKILL.md +135 -0
- package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
- package/templates/project/backend/.dwy.hbs +5 -0
- package/templates/project/backend/.python-version +1 -0
- package/templates/project/backend/CLAUDE.md.hbs +40 -0
- package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
- package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
- package/templates/project/backend/deploy.sh.hbs +18 -0
- package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
- package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
- package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
- package/templates/project/backend/src/app/database.py +10 -0
- package/templates/project/backend/src/app/dependencies.py +5 -0
- package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
- package/templates/project/backend/src/app/models/base.py +5 -0
- package/templates/project/web/CLAUDE.md.hbs +29 -36
- package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
- package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
- package/templates/project/web/apps/web/src/App.vue +9 -0
- package/templates/project/web/apps/web/src/api/client.ts +5 -0
- package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
- package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
- package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
- package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
- package/templates/project/web/apps/web/src/style.css +2 -0
- package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
- package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
- package/templates/project/web/apps/web/tsconfig.app.json +18 -0
- package/templates/project/web/apps/web/tsconfig.json +6 -0
- package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
- package/templates/project/web/package.json.hbs +16 -0
- package/templates/project/web/packages/.gitkeep +0 -0
- package/templates/project/web/pnpm-workspace.yaml +3 -0
- package/templates/claude-global/rules/agent-team-execution.md +0 -138
- package/templates/claude-global/rules/agent-team-patterns.md +0 -157
- package/templates/claude-global/rules/git-security.md +0 -144
- package/templates/claude-global/skills/dwy-auto-sync/SKILL.md +0 -52
- package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
- package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
- package/templates/project/web/.claude/commands/deploy.md +0 -4
- package/templates/project/web/backend/src/app/models/base.py +0 -5
- package/templates/project/web/deploy.sh.hbs +0 -113
- package/templates/project/web/frontend/src/App.vue +0 -3
- package/templates/project/web/frontend/src/api/client.ts +0 -13
- package/templates/project/web/frontend/src/style.css +0 -1
- package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
- package/templates/project/web/frontend/tsconfig.app.json +0 -14
- package/templates/project/web/frontend/tsconfig.json +0 -4
- /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
- /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
- /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
- /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
- /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
- /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
- /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/main.ts +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Flutter Dio 网络层 + 拦截器 + 错误处理
|
|
3
|
+
paths:
|
|
4
|
+
- "**/core/network/**/*.dart"
|
|
5
|
+
- "**/dio_client.dart"
|
|
6
|
+
- "**/interceptors/**/*.dart"
|
|
7
|
+
- "**/network/**/*.dart"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Flutter 网络层规范
|
|
11
|
+
|
|
12
|
+
## 一、网络层(Dio)
|
|
13
|
+
|
|
14
|
+
### 1.1 Dio 实例封装
|
|
15
|
+
|
|
16
|
+
```dart
|
|
17
|
+
// core/network/dio_client.dart
|
|
18
|
+
import 'package:dio/dio.dart';
|
|
19
|
+
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
20
|
+
part 'dio_client.g.dart';
|
|
21
|
+
|
|
22
|
+
@Riverpod(keepAlive: true)
|
|
23
|
+
Dio dio(Ref ref) {
|
|
24
|
+
final env = ref.watch(envProvider);
|
|
25
|
+
final dio = Dio(BaseOptions(
|
|
26
|
+
baseUrl: env.apiBaseUrl,
|
|
27
|
+
connectTimeout: const Duration(seconds: 10),
|
|
28
|
+
receiveTimeout: const Duration(seconds: 30),
|
|
29
|
+
headers: {'Content-Type': 'application/json'},
|
|
30
|
+
));
|
|
31
|
+
|
|
32
|
+
dio.interceptors.addAll([
|
|
33
|
+
TokenInterceptor(ref),
|
|
34
|
+
RefreshInterceptor(ref),
|
|
35
|
+
UnwrapInterceptor(),
|
|
36
|
+
ErrorInterceptor(ref),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
return dio;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 1.2 拦截器
|
|
44
|
+
|
|
45
|
+
**Token 注入:**
|
|
46
|
+
```dart
|
|
47
|
+
class TokenInterceptor extends Interceptor {
|
|
48
|
+
final Ref ref;
|
|
49
|
+
TokenInterceptor(this.ref);
|
|
50
|
+
|
|
51
|
+
@override
|
|
52
|
+
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
|
53
|
+
final token = await TokenStorage.getAccessToken();
|
|
54
|
+
if (token != null) {
|
|
55
|
+
options.headers['Authorization'] = 'Bearer $token';
|
|
56
|
+
}
|
|
57
|
+
handler.next(options);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**响应解包(对齐 eapi 格式):**
|
|
63
|
+
```dart
|
|
64
|
+
class UnwrapInterceptor extends Interceptor {
|
|
65
|
+
@override
|
|
66
|
+
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
|
67
|
+
final data = response.data;
|
|
68
|
+
if (data is Map && data.containsKey('code') && data.containsKey('data')) {
|
|
69
|
+
// eapi 格式: {code: "SUCCESS", message: "success", data: {...}, timestamp: ...}
|
|
70
|
+
response.data = data['data'];
|
|
71
|
+
}
|
|
72
|
+
handler.next(response);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**401 Token 刷新(使用 QueuedInterceptor 防止竞态):**
|
|
78
|
+
|
|
79
|
+
> **关键:** 使用 `QueuedInterceptor` 而非 `Interceptor`。QueuedInterceptor 串行处理请求,
|
|
80
|
+
> 刷新期间后续 401 请求自动排队等待,无需手动管理队列。
|
|
81
|
+
> 刷新 token 必须用**独立 Dio 实例**,避免被自身拦截器死锁。
|
|
82
|
+
|
|
83
|
+
```dart
|
|
84
|
+
class RefreshInterceptor extends QueuedInterceptor {
|
|
85
|
+
final Ref ref;
|
|
86
|
+
final Dio _refreshDio = Dio(); // 独立实例,不经过拦截器链
|
|
87
|
+
|
|
88
|
+
RefreshInterceptor(this.ref);
|
|
89
|
+
|
|
90
|
+
@override
|
|
91
|
+
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
|
92
|
+
if (err.response?.statusCode != 401) {
|
|
93
|
+
return handler.next(err);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
final refreshToken = await TokenStorage.getRefreshToken();
|
|
98
|
+
if (refreshToken == null) {
|
|
99
|
+
ref.read(authProvider.notifier).logout();
|
|
100
|
+
return handler.next(err);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 用独立 Dio 实例刷新 token
|
|
104
|
+
final response = await _refreshDio.post(
|
|
105
|
+
'${ref.read(envProvider).apiBaseUrl}/api/auth/refresh',
|
|
106
|
+
data: {'refresh_token': refreshToken},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
final newToken = response.data['data']['access_token'];
|
|
110
|
+
await TokenStorage.saveAccessToken(newToken);
|
|
111
|
+
|
|
112
|
+
// 用新 token 重试原请求
|
|
113
|
+
err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
|
|
114
|
+
final retryResponse = await ref.read(dioProvider).fetch(err.requestOptions);
|
|
115
|
+
handler.resolve(retryResponse);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// 刷新失败 → 登出
|
|
118
|
+
ref.read(authProvider.notifier).logout();
|
|
119
|
+
handler.next(err);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 1.3 Repository 模式
|
|
126
|
+
|
|
127
|
+
```dart
|
|
128
|
+
// features/user/data/user_repository.dart
|
|
129
|
+
class UserRepository {
|
|
130
|
+
final Dio _dio;
|
|
131
|
+
UserRepository(this._dio);
|
|
132
|
+
|
|
133
|
+
Future<List<User>> getUsers({int page = 1, int pageSize = 20}) async {
|
|
134
|
+
final response = await _dio.get('/api/users', queryParameters: {
|
|
135
|
+
'page': page,
|
|
136
|
+
'page_size': pageSize,
|
|
137
|
+
});
|
|
138
|
+
final data = response.data as Map<String, dynamic>;
|
|
139
|
+
return (data['items'] as List).map((e) => User.fromJson(e)).toList();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Future<User> getUser(int id) async {
|
|
143
|
+
final response = await _dio.get('/api/users/$id');
|
|
144
|
+
return User.fromJson(response.data);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Future<void> deleteUser(int id) async {
|
|
148
|
+
await _dio.delete('/api/users/$id');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Provider 注入 Repository
|
|
153
|
+
@riverpod
|
|
154
|
+
UserRepository userRepository(Ref ref) {
|
|
155
|
+
return UserRepository(ref.watch(dioProvider));
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 1.4 禁止事项
|
|
160
|
+
|
|
161
|
+
- 禁止 `http.get()` / `HttpClient` / 裸 `Dio()` 不加拦截器
|
|
162
|
+
- 禁止在 Widget 中直接调用 Dio(通过 Provider + Repository)
|
|
163
|
+
- 禁止硬编码 API 地址(走环境配置)
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 二、错误处理(Flutter)
|
|
168
|
+
|
|
169
|
+
Dio interceptor 统一处理,模式与前端对齐:
|
|
170
|
+
|
|
171
|
+
```dart
|
|
172
|
+
class ErrorInterceptor extends Interceptor {
|
|
173
|
+
@override
|
|
174
|
+
void onError(DioException err, ErrorInterceptorHandler handler) {
|
|
175
|
+
switch (err.response?.statusCode) {
|
|
176
|
+
case 401: // → 跳转登录
|
|
177
|
+
case 403: // → 提示无权限
|
|
178
|
+
case 422: // → 提取 message 显示
|
|
179
|
+
}
|
|
180
|
+
handler.next(err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 三、网络层规范(跨端对齐)
|
|
188
|
+
|
|
189
|
+
### 3.1 Flutter 唯一方式
|
|
190
|
+
|
|
191
|
+
**唯一方式:** Dio + 拦截器,封装与 ekit request 对齐。
|
|
192
|
+
|
|
193
|
+
```dart
|
|
194
|
+
// core/network/dio_client.dart
|
|
195
|
+
class DioClient {
|
|
196
|
+
late final Dio _dio;
|
|
197
|
+
|
|
198
|
+
DioClient({required String baseUrl}) {
|
|
199
|
+
_dio = Dio(BaseOptions(baseUrl: baseUrl))
|
|
200
|
+
..interceptors.addAll([
|
|
201
|
+
TokenInterceptor(), // 注入 Authorization header
|
|
202
|
+
RefreshInterceptor(), // 401 自动刷新
|
|
203
|
+
UnwrapInterceptor(), // 解包 {code, data, message} → data
|
|
204
|
+
ErrorInterceptor(), // 统一错误处理
|
|
205
|
+
]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**禁止:** `http.get()`、`HttpClient`、裸 `Dio()` 不加拦截器
|
|
211
|
+
|
|
212
|
+
### 3.2 响应解包
|
|
213
|
+
|
|
214
|
+
前端和 Flutter 的解包逻辑必须与后端 eapi response 格式对齐:
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
后端返回: { code: "SUCCESS", message: "success", data: {...}, timestamp: ... }
|
|
218
|
+
↓ unwrap
|
|
219
|
+
前端/Flutter 拿到: {...} (直接是 data 的内容)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
分页响应解包后拿到 `{ items, total, page, page_size }`(保持 snake_case 对齐后端 JSON)。
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Flutter GoRouter 路由规范
|
|
3
|
+
paths:
|
|
4
|
+
- "**/app.dart"
|
|
5
|
+
- "**/router/**/*.dart"
|
|
6
|
+
- "**/routes/**/*.dart"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Flutter 路由规范
|
|
10
|
+
|
|
11
|
+
## 一、路由(GoRouter)
|
|
12
|
+
|
|
13
|
+
### 1.1 路由定义
|
|
14
|
+
|
|
15
|
+
```dart
|
|
16
|
+
// app.dart 或 shared/providers/router_provider.dart
|
|
17
|
+
@riverpod
|
|
18
|
+
GoRouter router(Ref ref) {
|
|
19
|
+
final auth = ref.watch(authProvider);
|
|
20
|
+
|
|
21
|
+
return GoRouter(
|
|
22
|
+
initialLocation: '/',
|
|
23
|
+
redirect: (context, state) {
|
|
24
|
+
final isLoggedIn = auth.isAuthenticated;
|
|
25
|
+
final isLoginRoute = state.matchedLocation == '/login';
|
|
26
|
+
|
|
27
|
+
if (!isLoggedIn && !isLoginRoute) return '/login';
|
|
28
|
+
if (isLoggedIn && isLoginRoute) return '/';
|
|
29
|
+
return null;
|
|
30
|
+
},
|
|
31
|
+
routes: [
|
|
32
|
+
GoRoute(
|
|
33
|
+
path: '/login',
|
|
34
|
+
builder: (context, state) => const LoginPage(),
|
|
35
|
+
),
|
|
36
|
+
ShellRoute(
|
|
37
|
+
builder: (context, state, child) => AppShell(child: child),
|
|
38
|
+
routes: [
|
|
39
|
+
GoRoute(
|
|
40
|
+
path: '/',
|
|
41
|
+
builder: (context, state) => const HomePage(),
|
|
42
|
+
),
|
|
43
|
+
GoRoute(
|
|
44
|
+
path: '/users',
|
|
45
|
+
builder: (context, state) => const UserListPage(),
|
|
46
|
+
),
|
|
47
|
+
GoRoute(
|
|
48
|
+
path: '/users/:id',
|
|
49
|
+
builder: (context, state) {
|
|
50
|
+
final id = int.parse(state.pathParameters['id']!);
|
|
51
|
+
return UserDetailPage(userId: id);
|
|
52
|
+
},
|
|
53
|
+
),
|
|
54
|
+
],
|
|
55
|
+
),
|
|
56
|
+
],
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 1.2 导航
|
|
62
|
+
|
|
63
|
+
```dart
|
|
64
|
+
// 声明式导航
|
|
65
|
+
context.go('/users'); // 替换当前页面栈
|
|
66
|
+
context.push('/users/123'); // 压入新页面
|
|
67
|
+
context.pop(); // 返回
|
|
68
|
+
|
|
69
|
+
// 禁止使用 Navigator.push / Navigator.pushNamed
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 1.3 底部导航(StatefulShellRoute)
|
|
73
|
+
|
|
74
|
+
```dart
|
|
75
|
+
StatefulShellRoute.indexedStack(
|
|
76
|
+
builder: (context, state, navigationShell) {
|
|
77
|
+
return ScaffoldWithBottomNav(navigationShell: navigationShell);
|
|
78
|
+
},
|
|
79
|
+
branches: [
|
|
80
|
+
StatefulShellBranch(routes: [
|
|
81
|
+
GoRoute(path: '/home', builder: (_, __) => const HomePage()),
|
|
82
|
+
]),
|
|
83
|
+
StatefulShellBranch(routes: [
|
|
84
|
+
GoRoute(path: '/profile', builder: (_, __) => const ProfilePage()),
|
|
85
|
+
]),
|
|
86
|
+
],
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 1.4 路由规则
|
|
91
|
+
|
|
92
|
+
| 规则 | 说明 |
|
|
93
|
+
|------|------|
|
|
94
|
+
| 全部用 GoRouter | 禁止 `Navigator.push` / `Navigator.pushNamed` |
|
|
95
|
+
| 认证守卫在 redirect 中 | 不在每个页面单独检查 |
|
|
96
|
+
| 路径参数用 `:id` | `/users/:id`,通过 `state.pathParameters['id']` 获取 |
|
|
97
|
+
| 查询参数用 `state.uri.queryParameters` | 分页、筛选等 |
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Flutter / Dart 测试规范(mocktail + ProviderContainer + Widget 测试)
|
|
3
|
+
paths:
|
|
4
|
+
- "**/test/**/*.dart"
|
|
5
|
+
- "**/*_test.dart"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Flutter 测试规范
|
|
9
|
+
|
|
10
|
+
## 一、测试分类
|
|
11
|
+
|
|
12
|
+
| 类型 | 位置 | 工具 | 重点 |
|
|
13
|
+
|------|------|------|------|
|
|
14
|
+
| Unit Test | `test/features/{name}/` | flutter_test + mocktail | Repository、Service、工具函数 |
|
|
15
|
+
| Provider Test | `test/features/{name}/` | flutter_test + ProviderContainer | Provider 状态变化 |
|
|
16
|
+
| Widget Test | `test/features/{name}/` | flutter_test + ProviderScope.overrides | 页面渲染和交互 |
|
|
17
|
+
|
|
18
|
+
## 二、Provider 测试
|
|
19
|
+
|
|
20
|
+
```dart
|
|
21
|
+
import 'package:flutter_test/flutter_test.dart';
|
|
22
|
+
import 'package:mocktail/mocktail.dart';
|
|
23
|
+
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
24
|
+
|
|
25
|
+
class MockUserRepository extends Mock implements UserRepository {}
|
|
26
|
+
|
|
27
|
+
void main() {
|
|
28
|
+
late MockUserRepository mockRepo;
|
|
29
|
+
late ProviderContainer container;
|
|
30
|
+
|
|
31
|
+
setUp(() {
|
|
32
|
+
mockRepo = MockUserRepository();
|
|
33
|
+
container = ProviderContainer(overrides: [
|
|
34
|
+
userRepositoryProvider.overrideWithValue(mockRepo),
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
tearDown(() => container.dispose());
|
|
39
|
+
|
|
40
|
+
test('userList 初始化时获取用户列表', () async {
|
|
41
|
+
when(() => mockRepo.getUsers()).thenAnswer(
|
|
42
|
+
(_) async => [User(id: 1, username: 'test', role: 'admin', isActive: true, createdAt: DateTime.now())],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// 读取 provider 触发初始化
|
|
46
|
+
final future = container.read(userListProvider.future);
|
|
47
|
+
final users = await future;
|
|
48
|
+
|
|
49
|
+
expect(users, hasLength(1));
|
|
50
|
+
expect(users.first.username, 'test');
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 三、Widget 测试
|
|
56
|
+
|
|
57
|
+
```dart
|
|
58
|
+
testWidgets('登录页面渲染正确', (tester) async {
|
|
59
|
+
await tester.pumpWidget(
|
|
60
|
+
ProviderScope(
|
|
61
|
+
overrides: [
|
|
62
|
+
authProvider.overrideWith(() => MockAuth()),
|
|
63
|
+
],
|
|
64
|
+
child: const MaterialApp(home: LoginPage()),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(find.byType(TextFormField), findsNWidgets(2));
|
|
69
|
+
expect(find.text('登录'), findsOneWidget);
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 四、Mock 工具
|
|
74
|
+
|
|
75
|
+
统一使用 `mocktail`(不是 mockito):
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
dev_dependencies:
|
|
79
|
+
mocktail: ^1.0.0
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```dart
|
|
83
|
+
class MockDio extends Mock implements Dio {}
|
|
84
|
+
class MockAuthRepository extends Mock implements AuthRepository {}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 五、测试规则
|
|
88
|
+
|
|
89
|
+
| 规则 | 说明 |
|
|
90
|
+
|------|------|
|
|
91
|
+
| 每个 feature 有对应测试目录 | `test/features/{name}/` |
|
|
92
|
+
| Repository 和 Provider 必须测试 | 业务逻辑的核心 |
|
|
93
|
+
| Mock 用 mocktail | 禁止 mockito(mocktail 无需 codegen) |
|
|
94
|
+
| 遵循项目 TDD 流程 | 先写测试 → 再实现 → 回归 |
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Git commit message 规范(type 枚举、scope、中文 subject、提交前检查)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Git Commit 强制规范
|
|
6
|
+
|
|
7
|
+
每次 `git commit` 前按顺序执行以下检查,任一步未通过则 **STOP,不得提交**。
|
|
8
|
+
|
|
9
|
+
## Step 1: 分析变更
|
|
10
|
+
|
|
11
|
+
`git diff --cached` 回答:这次改做了几件事?涉及哪些模块?
|
|
12
|
+
|
|
13
|
+
**做了多件事(无法用 ≤72 字符 subject 清晰描述)→ 必须拆成多个 commit。**
|
|
14
|
+
|
|
15
|
+
## Step 2: 确定 scope
|
|
16
|
+
|
|
17
|
+
读当前项目 CLAUDE.md 的 `## Git Commit Scope` 段落:
|
|
18
|
+
|
|
19
|
+
- **找到** → scope 从其中枚举值选,不得自造
|
|
20
|
+
- **没找到** → 提醒用户:「当前项目 CLAUDE.md 未定义 Git Commit Scope,建议添加」,scope 可省略
|
|
21
|
+
|
|
22
|
+
单模块变更 → **必须**带 scope;跨模块变更 → 省略 scope。
|
|
23
|
+
|
|
24
|
+
## Step 3: 生成 message
|
|
25
|
+
|
|
26
|
+
格式:`<type>(<scope>): <subject>` 或(无 scope)`<type>: <subject>`
|
|
27
|
+
|
|
28
|
+
**type 枚举(只能从中选)**:
|
|
29
|
+
|
|
30
|
+
| type | 用途 |
|
|
31
|
+
|------|------|
|
|
32
|
+
| feat | 新功能 |
|
|
33
|
+
| fix | 修复 bug |
|
|
34
|
+
| refactor | 重构(不改外部行为) |
|
|
35
|
+
| chore | 构建/依赖/配置/发版等杂务 |
|
|
36
|
+
| docs | 文档 |
|
|
37
|
+
| test | 测试 |
|
|
38
|
+
| perf | 性能优化 |
|
|
39
|
+
| style | 代码格式(不影响逻辑) |
|
|
40
|
+
| ci | CI/CD 配置 |
|
|
41
|
+
|
|
42
|
+
**subject**:中文、无句号、≤72 字符、动宾短语(添加/修复/移除/重构/升级)。
|
|
43
|
+
|
|
44
|
+
**body(可选)**:空一行后写;中文;只写 why 不复述 what;破坏性变更加 `BREAKING CHANGE:` 前缀(subject 用 `feat!:` / `fix!:` 标记)。
|
|
45
|
+
|
|
46
|
+
## Step 4: 禁止大模型署名
|
|
47
|
+
|
|
48
|
+
commit message(subject、body、footer、trailer)**禁止出现任何大模型/AI 工具相关内容**,覆盖系统默认的 `Co-Authored-By` 追加行为。
|
|
49
|
+
|
|
50
|
+
**禁止出现的内容(不完全列举)**:
|
|
51
|
+
|
|
52
|
+
- `Co-Authored-By: Claude ...` / `Co-Authored-By: GPT ...` / `Co-Authored-By: Copilot ...` 等 AI 署名 trailer
|
|
53
|
+
- `Generated with Claude Code` / `🤖 Generated with ...` 等生成声明
|
|
54
|
+
- `noreply@anthropic.com` / `noreply@openai.com` 等 AI 厂商邮箱
|
|
55
|
+
- 任何提到 Claude / ChatGPT / GPT / Copilot / Cursor / AI / 大模型 / LLM 的字样
|
|
56
|
+
|
|
57
|
+
**执行要点**:
|
|
58
|
+
|
|
59
|
+
- `git commit -m "..."` 只写规范内容,**绝不**附加 `Co-Authored-By` trailer
|
|
60
|
+
- 使用 HEREDOC 传 commit message 时,HEREDOC 内容也不得包含上述字样
|
|
61
|
+
- 若发现暂存的 message 含大模型信息,**立即删除**后再提交
|
|
62
|
+
|
|
63
|
+
## 常见错误 vs 正确
|
|
64
|
+
|
|
65
|
+
| 错误 | 正确 |
|
|
66
|
+
|------|------|
|
|
67
|
+
| `feat: add user login`(英文) | `feat: 添加用户登录功能` |
|
|
68
|
+
| `fix(eui): 修复了按钮样式。` | `fix(eui): 修复按钮样式`(无句号、不用"了") |
|
|
69
|
+
| `feat: 添加登录并修复登出 bug` | 拆成两个 commit |
|
|
70
|
+
| `update: 修改配置` | `update` 不在枚举,应为 `chore` |
|
|
71
|
+
| `feat(auth): ...`(项目无 auth scope) | 用项目已定义的 scope 值 |
|
|
72
|
+
| message 末尾加 `Co-Authored-By: Claude ...` | 不加任何 AI 署名 trailer |
|
|
73
|
+
| `🤖 Generated with Claude Code` | 不加任何生成声明 |
|
|
74
|
+
|
|
75
|
+
## 正确示例
|
|
76
|
+
|
|
77
|
+
- `feat(eui): 添加 Image 组件,支持懒加载`
|
|
78
|
+
- `fix: 修复 token 刷新竞态条件`
|
|
79
|
+
- `refactor(backend): 提取分页逻辑为共享工具`
|
|
80
|
+
- `chore: 升级 Vite 至 8.x`
|
|
81
|
+
- `feat(eui)!: 重命名 EDialog open 属性为 v-model:open`
|
|
82
|
+
- body:`BREAKING CHANGE: EDialog 不再接受 visible 属性,请改用 v-model:open`
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Git 提交前文件与敏感内容扫描规则(全局,每次 add/commit 都应检查)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Git 提交安全规则
|
|
6
|
+
|
|
7
|
+
每次 `git add` 或 `git commit` 前按本规则扫描暂存区。发现违规**立即停止并提示用户确认**。
|
|
8
|
+
|
|
9
|
+
## 一、禁止提交的文件
|
|
10
|
+
|
|
11
|
+
| 类型 | 示例 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| 环境变量 | `.env`, `.env.*`, `*.env` |
|
|
14
|
+
| SSH 密钥 | `id_rsa`, `id_ed25519`, `*.pem`, `*.key` |
|
|
15
|
+
| 证书 | `*.p12`, `*.pfx`, `*.jks`, `*.keystore` |
|
|
16
|
+
| 数据库 | `*.sql`(含真实数据)、`*.dump`, `*.sqlite` |
|
|
17
|
+
| 原始数据 | `*.xlsx`, `*.csv`(含用户/学生等敏感数据) |
|
|
18
|
+
| 日志 | `*.log` |
|
|
19
|
+
| Docker 卷 | `pgdata/`, `cache/*.json` |
|
|
20
|
+
|
|
21
|
+
## 二、禁止提交的内容(敏感模式)
|
|
22
|
+
|
|
23
|
+
**总则**:代码、文档、规则文件、注释里禁止出现真实的 API Key / Token / 密码 / IP / 凭证,即使作为"示例"也不行。GitHub Secret Scanning 不区分上下文,真实格式的 key 写在文档里也会触发泄露告警。示例必须用明显的假值(`sk-xxxxxxxxxxxx`、`your-key-here`、`192.168.x.x`)。
|
|
24
|
+
|
|
25
|
+
| 类型 | 匹配模式 |
|
|
26
|
+
|---|---|
|
|
27
|
+
| API Key | `sk-[a-zA-Z0-9]{20,}` |
|
|
28
|
+
| AWS Key | `AKIA[0-9A-Z]{16}` |
|
|
29
|
+
| 密码赋值 | `password\s*[:=]\s*["'][^"']{4,}` |
|
|
30
|
+
| 数据库连接串 | `://\w+:[^@\s]+@` |
|
|
31
|
+
| 私钥内容 | `-----BEGIN (RSA \|EC )?PRIVATE KEY-----` |
|
|
32
|
+
| Token | `token\s*[:=]\s*["'][a-zA-Z0-9_\-]{20,}` |
|
|
33
|
+
| Secret | `secret\s*[:=]\s*["'][^"']{8,}` |
|
|
34
|
+
| 服务器 IP+端口 | `\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+` |
|
|
35
|
+
| SSH 连接 | `ssh\s+.*@\d{1,3}\.\d{1,3}` |
|
|
36
|
+
|
|
37
|
+
**例外(不算违规)**:`.env.example` 里的占位符;从环境变量读取(`os.getenv("API_KEY")`);`localhost`/`127.0.0.1` 的连接串;文档里的假数据;配置文件的空字符串默认值。
|
|
38
|
+
|
|
39
|
+
## 三、违规处理
|
|
40
|
+
|
|
41
|
+
检测到违规时:
|
|
42
|
+
|
|
43
|
+
1. **立即停止** `git add` / `git commit`
|
|
44
|
+
2. **列出违规项**:文件 + 行号 + 匹配的敏感模式 + 脱敏摘要(如 `sk-5522****dc3`)
|
|
45
|
+
3. **建议**:加入 `.gitignore` / 移到 `.env` / 用环境变量
|
|
46
|
+
4. **等用户明确确认**后才能继续;确认提交时,commit body 附注释 `GIT-SECURITY: 用户已确认提交此内容`
|
|
47
|
+
|
|
48
|
+
## 四、已提交敏感数据
|
|
49
|
+
|
|
50
|
+
若发现已在 git 历史中:立即通知用户,**建议先轮换凭证**,然后提供清理命令(不自动执行,待确认):
|
|
51
|
+
|
|
52
|
+
- `git filter-repo --path <file> --invert-paths --force`
|
|
53
|
+
- 或 `bfg --delete-files <filename>`
|