fix(backend): JWT config mapstructure tag 修复 + 模型从 hotel 重构为 shop
- 修复 JWTConfig 缺少 mapstructure tag 导致 access_expire_min 解析为 0, token 签发即过期,所有 API 请求返回 401 - 全部 config struct 补齐 mapstructure tag(secret/dsn/hmac_secret 等) - 模型层从 hotel/HotelID 统一重命名为 shop/ShopID - 删除旧 migrations(001-004),新增 001_init 综合迁移文件 - 更新 schema.sql、testutil、handler/service/model 相关引用 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+120
-10
@@ -14,6 +14,7 @@ tools: Read, Write, Edit, Glob, Grep, Bash
|
||||
- **API 文档即契约**:严格按 `docs/api/{功能名称}.md` 对接接口,字段名、类型不得自行修改
|
||||
- **组件复用优先**:新 UI 优先复用 `client/lib/widgets/` 中已有组件
|
||||
- **响应式设计**:同时考虑桌面(宽屏)和移动端(窄屏)布局
|
||||
- **可测试性(强制要求)**:所有可交互元素必须便于 Widget Test 定位,见下方规范
|
||||
|
||||
## 开始前必读
|
||||
|
||||
@@ -38,7 +39,7 @@ final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<Product>>(
|
||||
|
||||
```dart
|
||||
// client/lib/core/api/api_client.dart 已封装基础请求
|
||||
// 新功能在对应 provider 中调用,不直接在 Widget 中写 http 请求
|
||||
// 新功能在对应 repository/provider 中调用,不直接在 Widget 中写 http 请求
|
||||
```
|
||||
|
||||
**页面结构规范**:
|
||||
@@ -46,7 +47,7 @@ final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<Product>>(
|
||||
```dart
|
||||
// screens/{模块}/{功能}_screen.dart — 页面
|
||||
// screens/{模块}/{功能}_form.dart — 表单弹窗
|
||||
// 复杂表格用 widgets/data_table_widget.dart
|
||||
// 复杂表格用 widgets/data_table_card.dart
|
||||
```
|
||||
|
||||
**UI 风格**(参考截图中的参考系统):
|
||||
@@ -60,22 +61,23 @@ final productsProvider = AsyncNotifierProvider<ProductsNotifier, List<Product>>(
|
||||
|
||||
```
|
||||
client/lib/
|
||||
├── models/{功能}.dart # 数据模型(与 API 响应字段对应)
|
||||
├── providers/{功能}_provider.dart # Riverpod 状态
|
||||
├── models/{功能}.dart # 数据模型(与 API 响应字段对应)
|
||||
├── repositories/{功能}_repository.dart # HTTP 调用层
|
||||
├── providers/{功能}_provider.dart # Riverpod 状态
|
||||
└── screens/{模块}/
|
||||
├── {功能}_screen.dart # 列表/主页面
|
||||
└── {功能}_form.dart # 新建/编辑表单
|
||||
├── {功能}_screen.dart # 列表/主页面
|
||||
└── {功能}_form.dart # 新建/编辑表单
|
||||
```
|
||||
|
||||
## 数据模型规范
|
||||
|
||||
```dart
|
||||
// 使用 freezed 或手写 fromJson/toJson
|
||||
// 使用手写 fromJson/toJson(项目未引入 freezed/json_serializable)
|
||||
class Product {
|
||||
final int id;
|
||||
final String name;
|
||||
final String? series;
|
||||
final Map<String, dynamic>? customFields; // 对应 custom_fields
|
||||
final Map<String, dynamic>? customFields;
|
||||
|
||||
const Product({required this.id, required this.name, this.series, this.customFields});
|
||||
|
||||
@@ -85,11 +87,119 @@ class Product {
|
||||
series: json['series'] as String?,
|
||||
customFields: json['custom_fields'] as Map<String, dynamic>?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'name': name,
|
||||
if (series != null) 'series': series,
|
||||
if (customFields != null) 'custom_fields': customFields,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 可测试性规范(强制执行)
|
||||
|
||||
test-engineer 会对每个按钮写 Widget Test,你必须确保所有可交互元素**可被 `find` 定位**:
|
||||
|
||||
### 按钮文字唯一性
|
||||
|
||||
按钮必须有**唯一且语义明确**的文字或 Key,避免只用图标:
|
||||
|
||||
```dart
|
||||
// ✅ 好:有文字,可被 find.text() 定位
|
||||
ElevatedButton.icon(
|
||||
onPressed: _onCreate,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('新建'),
|
||||
)
|
||||
|
||||
// ✅ 好:纯图标按钮加 Key
|
||||
IconButton(
|
||||
key: const Key('btn_edit_product'),
|
||||
onPressed: _onEdit,
|
||||
icon: const Icon(Icons.edit),
|
||||
)
|
||||
|
||||
// ❌ 差:多个同名按钮,无法区分
|
||||
IconButton(onPressed: _onEdit, icon: const Icon(Icons.edit)) // 列表每行都有
|
||||
```
|
||||
|
||||
### 表单字段必须有 labelText 或 hintText
|
||||
|
||||
```dart
|
||||
// ✅ 可被 find.widgetWithText(TextFormField, '商品名称') 定位
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: '商品名称', hintText: '请输入商品名称'),
|
||||
validator: (v) => (v == null || v.isEmpty) ? '不能为空' : null,
|
||||
)
|
||||
```
|
||||
|
||||
### 对话框按钮文字固定
|
||||
|
||||
确认类对话框按钮文字统一,方便测试定位:
|
||||
- 确认删除:`'删除'` + `'取消'`
|
||||
- 保存表单:`'保存'` + `'取消'`
|
||||
- 审核操作:`'通过'` + `'拒绝'` + `'取消'`
|
||||
|
||||
### 异步状态必须有可检测的 Widget
|
||||
|
||||
```dart
|
||||
// 加载中
|
||||
if (state.isLoading) return const CircularProgressIndicator();
|
||||
|
||||
// 错误
|
||||
if (state.hasError) return Column(children: [
|
||||
Text('加载失败:${state.error}'),
|
||||
ElevatedButton(onPressed: _retry, child: const Text('重试')),
|
||||
]);
|
||||
|
||||
// 空列表
|
||||
if (items.isEmpty) return const Center(child: Text('暂无数据'));
|
||||
```
|
||||
|
||||
### 错误提示用 SnackBar 统一格式
|
||||
|
||||
```dart
|
||||
// 操作失败统一用 SnackBar,便于测试断言
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('错误:$message')),
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误处理规范
|
||||
|
||||
所有 API 调用必须处理错误,不得让异常冒泡到 UI 未处理:
|
||||
|
||||
```dart
|
||||
Future<void> _onSave() async {
|
||||
try {
|
||||
await ref.read(productsProvider.notifier).create(_formData);
|
||||
if (mounted) Navigator.pop(context); // 成功:关闭表单
|
||||
} on AppException catch (e) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.message)), // 业务错误:提示
|
||||
);
|
||||
} catch (_) {
|
||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('网络异常,请重试')),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完成后必做
|
||||
|
||||
1. 确认 `flutter analyze` 无 error(warning 可以有但要说明)
|
||||
2. 在新路由处注册页面(`client/lib/core/router/`)
|
||||
1. `flutter analyze` 无 error(warning 可以有但要说明)
|
||||
2. 在新路由处注册页面(`client/lib/core/router/app_router.dart`)
|
||||
3. 确认在桌面宽度(1280px+)和移动端宽度(375px)下布局正常
|
||||
4. **自查可测试性 checklist**:
|
||||
- [ ] 每个按钮有唯一文字或 Key
|
||||
- [ ] 每个表单字段有 labelText 和 validator(含"不能为空"错误文字)
|
||||
- [ ] 加载中/加载失败/空数据三种状态都有对应 Widget
|
||||
- [ ] 操作成功/失败有 SnackBar 提示
|
||||
- [ ] 删除/审核等危险操作有确认对话框
|
||||
|
||||
Reference in New Issue
Block a user