@robsun/create-keystone-app 0.2.13 → 0.2.15
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/README.md +1 -0
- package/dist/create-keystone-app.js +0 -0
- package/dist/create-module.js +584 -2
- package/package.json +22 -23
- package/template/.claude/skills/keystone-dev/SKILL.md +90 -103
- package/template/.claude/skills/keystone-dev/references/ADVANCED_PATTERNS.md +716 -0
- package/template/.claude/skills/keystone-dev/references/CHECKLIST.md +285 -0
- package/template/.claude/skills/keystone-dev/references/GOTCHAS.md +390 -0
- package/template/.claude/skills/keystone-dev/references/PATTERNS.md +605 -0
- package/template/.claude/skills/keystone-dev/references/TEMPLATES.md +2562 -384
- package/template/.codex/skills/keystone-dev/SKILL.md +90 -103
- package/template/.codex/skills/keystone-dev/references/ADVANCED_PATTERNS.md +716 -0
- package/template/.codex/skills/keystone-dev/references/CHECKLIST.md +285 -0
- package/template/.codex/skills/keystone-dev/references/GOTCHAS.md +390 -0
- package/template/.codex/skills/keystone-dev/references/PATTERNS.md +605 -0
- package/template/.codex/skills/keystone-dev/references/TEMPLATES.md +2562 -384
- package/template/README.md +8 -1
- package/template/docs/CONVENTIONS.md +11 -8
- package/template/package.json +3 -3
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Keystone 模块开发自检清单
|
|
2
|
+
|
|
3
|
+
> 完成开发后逐项检查,确保模块完整且符合规范。
|
|
4
|
+
|
|
5
|
+
## 1. 后端检查
|
|
6
|
+
|
|
7
|
+
### 1.1 文件结构 ✓
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
[ ] module.go - 实现所有 Module 接口方法
|
|
11
|
+
[ ] api/handler/ - Handler 结构体 + CRUD 方法
|
|
12
|
+
[ ] domain/models/ - 模型定义 + TableName()
|
|
13
|
+
[ ] domain/service/ - Service + Input/UpdateInput 类型
|
|
14
|
+
[ ] domain/service/errors.go - I18n 错误定义
|
|
15
|
+
[ ] infra/repository/ - Repository 实现 Service 定义的接口
|
|
16
|
+
[ ] i18n/keys.go - 翻译键常量
|
|
17
|
+
[ ] i18n/i18n.go - RegisterLocales() 函数
|
|
18
|
+
[ ] i18n/locales/ - zh-CN.json + en-US.json
|
|
19
|
+
[ ] bootstrap/migrations/ - Migrate() 函数
|
|
20
|
+
[ ] bootstrap/seeds/ - Seed() 函数(可选)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 1.2 Module 接口 ✓
|
|
24
|
+
|
|
25
|
+
```go
|
|
26
|
+
[ ] Name() - 返回模块名(小写)
|
|
27
|
+
[ ] RegisterRoutes() - 注册 API 路由
|
|
28
|
+
[ ] RegisterModels() - 返回模型列表
|
|
29
|
+
[ ] RegisterPermissions() - 注册菜单 + 操作权限
|
|
30
|
+
[ ] RegisterI18n() - 调用 modulei18n.RegisterLocales()
|
|
31
|
+
[ ] RegisterJobs() - 注册后台任务(可为空)
|
|
32
|
+
[ ] Migrate() - 调用 migrations.Migrate()
|
|
33
|
+
[ ] Seed() - 调用 seeds.Seed()
|
|
34
|
+
[ ] ensureServices() - 延迟初始化 service
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 1.3 Handler 检查 ✓
|
|
38
|
+
|
|
39
|
+
```go
|
|
40
|
+
[ ] nil 检查 - if h == nil || h.svc == nil
|
|
41
|
+
[ ] 租户隔离 - tenantID := resolveTenantID(c)
|
|
42
|
+
[ ] 参数绑定 - c.ShouldBindJSON(&input)
|
|
43
|
+
[ ] ID 解析 - hcommon.ParseUintParam(c, "id")
|
|
44
|
+
[ ] I18n 错误处理 - errors.As(err, &i18nErr)
|
|
45
|
+
[ ] 正确 HTTP 状态码 - BadRequest/NotFound/InternalError
|
|
46
|
+
[ ] I18n 响应消息 - response.SuccessI18n/CreatedI18n
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 1.4 Service 检查 ✓
|
|
50
|
+
|
|
51
|
+
```go
|
|
52
|
+
[ ] 输入验证 - strings.TrimSpace(), 空值检查
|
|
53
|
+
[ ] 状态验证 - status.IsValid()
|
|
54
|
+
[ ] 租户 ID 设置 - entity.TenantID = tenantID
|
|
55
|
+
[ ] 返回 I18n 错误 - return nil, ErrNameRequired
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 1.5 Repository 检查 ✓
|
|
59
|
+
|
|
60
|
+
```go
|
|
61
|
+
[ ] 租户过滤 - WHERE tenant_id = ?
|
|
62
|
+
[ ] 上下文传递 - db.WithContext(ctx)
|
|
63
|
+
[ ] 404 转换 - gorm.ErrRecordNotFound → service.ErrNotFound
|
|
64
|
+
[ ] 排序 - ORDER BY created_at desc
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 1.6 模型检查 ✓
|
|
68
|
+
|
|
69
|
+
```go
|
|
70
|
+
[ ] 继承 BaseModel - models.BaseModel
|
|
71
|
+
[ ] GORM 标签 - gorm:"size:200;not null"
|
|
72
|
+
[ ] JSON 标签 - json:"name"
|
|
73
|
+
[ ] TableName() - 返回表名
|
|
74
|
+
[ ] 状态枚举 - type Status string + IsValid()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 1.7 权限注册 ✓
|
|
78
|
+
|
|
79
|
+
```go
|
|
80
|
+
[ ] 菜单权限 - reg.CreateMenuI18n("module:entity", ...)
|
|
81
|
+
[ ] 查看权限 - module:entity:view
|
|
82
|
+
[ ] 管理权限 - module:entity:manage
|
|
83
|
+
[ ] 或细分权限 - :create, :update, :delete, :export, :import
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 1.8 翻译文件 ✓
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
[ ] keys.go 常量完整 - 所有消息键都有定义
|
|
90
|
+
[ ] zh-CN.json - 所有键都有中文翻译
|
|
91
|
+
[ ] en-US.json - 所有键都有英文翻译
|
|
92
|
+
[ ] 翻译键格式 - module.entity.action
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 2. 前端检查
|
|
98
|
+
|
|
99
|
+
### 2.1 文件结构 ✓
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
[ ] index.ts - registerRoutes() 调用
|
|
103
|
+
[ ] routes.tsx - 路由 + menu handle + permission
|
|
104
|
+
[ ] types.ts - 实体类型定义
|
|
105
|
+
[ ] services/api.ts - API 调用函数
|
|
106
|
+
[ ] pages/ - 页面组件
|
|
107
|
+
[ ] locales/zh-CN/ - 中文翻译
|
|
108
|
+
[ ] locales/en-US/ - 英文翻译
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 2.2 路由检查 ✓
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
[ ] lazyNamed - 按需加载组件
|
|
115
|
+
[ ] withSuspense - Suspense 包装
|
|
116
|
+
[ ] menu.labelKey - 使用翻译键
|
|
117
|
+
[ ] menu.icon - Ant Design 图标
|
|
118
|
+
[ ] menu.permission - 权限码
|
|
119
|
+
[ ] breadcrumbKey - 面包屑翻译键
|
|
120
|
+
[ ] permission - 路由级权限
|
|
121
|
+
[ ] helpKey - 帮助文档键
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 2.3 API 服务检查 ✓
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
[ ] 正确导入 api - import { api } from '@robsun/keystone-web-core'
|
|
128
|
+
[ ] 正确类型 - ApiResponse<T>
|
|
129
|
+
[ ] 解构 data.data - const { data } = await api.get<...>(...); return data.data
|
|
130
|
+
[ ] 完整 CRUD - list, get, create, update, delete
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 2.4 页面组件检查 ✓
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
[ ] useTranslation - const { t } = useTranslation('module')
|
|
137
|
+
[ ] App.useApp - const { message } = App.useApp()
|
|
138
|
+
[ ] 加载状态 - loading, setLoading
|
|
139
|
+
[ ] 错误处理 - try/catch + message.error
|
|
140
|
+
[ ] Form.useForm - const [form] = Form.useForm<FormValues>()
|
|
141
|
+
[ ] 表单验证 - rules={[{ required: true, ... }]}
|
|
142
|
+
[ ] Modal destroyOnHidden - 不要用 destroyOnClose
|
|
143
|
+
[ ] 确认删除 - Popconfirm
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### 2.5 翻译文件检查 ✓
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
[ ] menu - 菜单标签
|
|
150
|
+
[ ] page.title - 页面标题
|
|
151
|
+
[ ] page.createButton - 创建按钮
|
|
152
|
+
[ ] table.* - 表格列标题
|
|
153
|
+
[ ] form.* - 表单标签和占位符
|
|
154
|
+
[ ] status.* - 状态枚举翻译
|
|
155
|
+
[ ] messages.* - 操作反馈消息
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 2.6 类型检查 ✓
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
[ ] 实体类型完整 - 所有字段都有定义
|
|
162
|
+
[ ] 状态联合类型 - type Status = 'active' | 'inactive'
|
|
163
|
+
[ ] id 使用 number - 后端是 uint
|
|
164
|
+
[ ] 时间使用 string - ISO 8601 格式
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 3. 注册检查
|
|
170
|
+
|
|
171
|
+
### 3.1 后端注册 ✓
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
[ ] manifest.go 导入 - import xxxmodule "app/internal/modules/xxx"
|
|
175
|
+
[ ] manifest.go 注册 - Register(xxxmodule.NewModule())
|
|
176
|
+
[ ] config.yaml 启用 - modules.enabled 包含模块名
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 3.2 前端注册 ✓
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
[ ] main.tsx 导入 - import './modules/xxx'
|
|
183
|
+
[ ] app.config.ts 启用 - modules.enabled 包含模块名
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 4. 质量检查
|
|
189
|
+
|
|
190
|
+
### 4.1 代码检查 ✓
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
[ ] go build ./... - 编译通过
|
|
194
|
+
[ ] go test ./... - 测试通过
|
|
195
|
+
[ ] go vet ./... - 无警告
|
|
196
|
+
[ ] pnpm typecheck - TypeScript 类型检查通过
|
|
197
|
+
[ ] pnpm lint - ESLint 检查通过
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 4.2 功能验证 ✓
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
[ ] 列表页加载 - 数据正确显示
|
|
204
|
+
[ ] 创建功能 - 表单提交成功
|
|
205
|
+
[ ] 编辑功能 - 数据回填 + 更新成功
|
|
206
|
+
[ ] 删除功能 - 确认后删除成功
|
|
207
|
+
[ ] 分页功能 - 翻页正常(如适用)
|
|
208
|
+
[ ] 权限控制 - 无权限时按钮/菜单隐藏
|
|
209
|
+
[ ] 多语言切换 - 中英文显示正确
|
|
210
|
+
[ ] 错误提示 - 验证失败显示正确消息
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## 5. 快速验证命令
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# 后端
|
|
219
|
+
go build ./...
|
|
220
|
+
go test ./... -v
|
|
221
|
+
go vet ./...
|
|
222
|
+
|
|
223
|
+
# 前端
|
|
224
|
+
pnpm -C apps/web typecheck
|
|
225
|
+
pnpm -C apps/web lint
|
|
226
|
+
pnpm -C apps/web build
|
|
227
|
+
|
|
228
|
+
# 运行验证
|
|
229
|
+
make dev # 启动后端
|
|
230
|
+
pnpm -C apps/web dev # 启动前端
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 6. 审批流检查(--with-approval)
|
|
236
|
+
|
|
237
|
+
### 6.1 后端审批 ✓
|
|
238
|
+
|
|
239
|
+
```go
|
|
240
|
+
[ ] 状态枚举完整 - draft/pending/approved/rejected/active/inactive
|
|
241
|
+
[ ] ApprovalInstanceID - 模型包含审批实例 ID 字段
|
|
242
|
+
[ ] RejectReason - 模型包含拒绝原因字段
|
|
243
|
+
[ ] service/approval.go - Submit() 和 Cancel() 方法
|
|
244
|
+
[ ] service/callback.go - OnApproved() 和 OnRejected() 实现
|
|
245
|
+
[ ] handler/approval.go - HTTP 处理器
|
|
246
|
+
[ ] 审批路由 - POST /:id/submit, POST /:id/cancel
|
|
247
|
+
[ ] 回调注册 - RegisterApprovalCallback() 在 module.go
|
|
248
|
+
[ ] 审批类型常量 - ApprovalBusinessType = "module_approval"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### 6.2 前端审批 ✓
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
[ ] ApprovalActions - 状态标签 + 提交/撤回按钮组件
|
|
255
|
+
[ ] 状态类型完整 - 'draft' | 'pending' | 'approved' | 'rejected'
|
|
256
|
+
[ ] API 函数 - submit{Pascal}(), cancel{Pascal}Approval()
|
|
257
|
+
[ ] 状态翻译 - status.draft/pending/approved/rejected
|
|
258
|
+
[ ] 操作翻译 - actions.submit/cancelApproval
|
|
259
|
+
[ ] 确认提示 - confirm.submit/cancel
|
|
260
|
+
[ ] 消息翻译 - messages.submitSuccess/cancelSuccess
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### 6.3 审批流验证 ✓
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
[ ] 草稿→提交 - 只有 draft 状态可提交
|
|
267
|
+
[ ] 审批中→撤回 - 只有 pending 状态可撤回
|
|
268
|
+
[ ] 回调幂等 - OnApproved/OnRejected 检查当前状态
|
|
269
|
+
[ ] 上下文传递 - CreateInstance 包含业务信息
|
|
270
|
+
[ ] 错误处理 - 状态不匹配返回 I18n 错误
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 7. 常见遗漏
|
|
276
|
+
|
|
277
|
+
| 遗漏项 | 后果 | 检查方法 |
|
|
278
|
+
|--------|------|----------|
|
|
279
|
+
| `app.config.ts` 未启用 | 菜单不显示 | 检查 modules.enabled |
|
|
280
|
+
| `config.yaml` 未启用 | 模块不加载 | 检查 modules.enabled |
|
|
281
|
+
| 缺少 TableName() | 表名错误 | 检查数据库表 |
|
|
282
|
+
| 缺少 RegisterI18n() | 翻译不生效 | 检查错误消息 |
|
|
283
|
+
| Handler nil 检查缺失 | 服务 panic | 启动时测试 API |
|
|
284
|
+
| 缺少租户过滤 | 数据泄露 | 检查 SQL 日志 |
|
|
285
|
+
| 翻译键不匹配 | 显示键而非文本 | 切换语言测试 |
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# Keystone 开发常见陷阱
|
|
2
|
+
|
|
3
|
+
> 开发中容易踩的坑和解决方案。
|
|
4
|
+
|
|
5
|
+
## 后端陷阱
|
|
6
|
+
|
|
7
|
+
### G1: 菜单/模块不显示
|
|
8
|
+
|
|
9
|
+
**症状**:模块代码写完,但菜单不出现或 API 404
|
|
10
|
+
|
|
11
|
+
**原因**:未在配置中启用模块
|
|
12
|
+
|
|
13
|
+
**解决**:
|
|
14
|
+
```yaml
|
|
15
|
+
# config.yaml
|
|
16
|
+
modules:
|
|
17
|
+
enabled:
|
|
18
|
+
- example
|
|
19
|
+
- your_new_module # 添加这行
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// app.config.ts
|
|
24
|
+
modules: {
|
|
25
|
+
enabled: ['example', 'your_new_module'] // 添加这里
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
### G2: Handler panic: nil pointer
|
|
32
|
+
|
|
33
|
+
**症状**:调用 API 时服务 panic
|
|
34
|
+
|
|
35
|
+
**原因**:Handler 或 Service 未初始化
|
|
36
|
+
|
|
37
|
+
**错误代码**:
|
|
38
|
+
```go
|
|
39
|
+
func (h *Handler) List(c *gin.Context) {
|
|
40
|
+
items, _ := h.svc.List(c) // h.svc 可能为 nil
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**正确代码**:
|
|
45
|
+
```go
|
|
46
|
+
func (h *Handler) List(c *gin.Context) {
|
|
47
|
+
if h == nil || h.svc == nil {
|
|
48
|
+
response.ServiceUnavailableI18n(c, modulei18n.MsgServiceUnavailable)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
// ...
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### G3: 数据跨租户泄露
|
|
58
|
+
|
|
59
|
+
**症状**:用户能看到其他租户的数据
|
|
60
|
+
|
|
61
|
+
**原因**:Repository 查询缺少租户过滤
|
|
62
|
+
|
|
63
|
+
**错误代码**:
|
|
64
|
+
```go
|
|
65
|
+
func (r *Repository) List(ctx context.Context) ([]Entity, error) {
|
|
66
|
+
var items []Entity
|
|
67
|
+
r.db.Find(&items) // 未过滤租户
|
|
68
|
+
return items, nil
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**正确代码**:
|
|
73
|
+
```go
|
|
74
|
+
func (r *Repository) List(ctx context.Context, tenantID uint) ([]Entity, error) {
|
|
75
|
+
var items []Entity
|
|
76
|
+
r.db.Where("tenant_id = ?", tenantID).Find(&items)
|
|
77
|
+
return items, nil
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### G4: 翻译键显示为原始键
|
|
84
|
+
|
|
85
|
+
**症状**:界面显示 `example.item.created` 而非翻译文本
|
|
86
|
+
|
|
87
|
+
**原因**:
|
|
88
|
+
1. 未调用 `RegisterI18n()`
|
|
89
|
+
2. 翻译 JSON 文件路径错误
|
|
90
|
+
3. 键名不匹配
|
|
91
|
+
|
|
92
|
+
**检查**:
|
|
93
|
+
```go
|
|
94
|
+
// module.go 中必须有
|
|
95
|
+
func (m *Module) RegisterI18n() error {
|
|
96
|
+
return modulei18n.RegisterLocales() // 确保调用
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// i18n/i18n.go
|
|
100
|
+
//go:embed locales/*.json // 确保路径正确
|
|
101
|
+
var translations embed.FS
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### G5: GORM 自动更新 UpdatedAt 失效
|
|
107
|
+
|
|
108
|
+
**症状**:更新记录后 `updated_at` 未变化
|
|
109
|
+
|
|
110
|
+
**原因**:使用 `Updates()` 时只更新传入字段
|
|
111
|
+
|
|
112
|
+
**解决**:使用 `Save()` 或显式设置
|
|
113
|
+
```go
|
|
114
|
+
// 方式一:使用 Save
|
|
115
|
+
r.db.Save(entity)
|
|
116
|
+
|
|
117
|
+
// 方式二:显式更新
|
|
118
|
+
r.db.Model(entity).Updates(map[string]interface{}{
|
|
119
|
+
"name": entity.Name,
|
|
120
|
+
"updated_at": time.Now(),
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### G6: 软删除查不到数据
|
|
127
|
+
|
|
128
|
+
**症状**:数据库有记录但查询返回空
|
|
129
|
+
|
|
130
|
+
**原因**:GORM 自动添加 `deleted_at IS NULL` 条件
|
|
131
|
+
|
|
132
|
+
**解决**:
|
|
133
|
+
```go
|
|
134
|
+
// 查询包含已删除记录
|
|
135
|
+
r.db.Unscoped().Where("id = ?", id).First(&entity)
|
|
136
|
+
|
|
137
|
+
// 恢复已删除记录
|
|
138
|
+
r.db.Unscoped().Model(&Entity{}).
|
|
139
|
+
Where("id = ?", id).
|
|
140
|
+
Update("deleted_at", nil)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### G7: 循环依赖导致编译失败
|
|
146
|
+
|
|
147
|
+
**症状**:`import cycle not allowed`
|
|
148
|
+
|
|
149
|
+
**原因**:service 和 repository 互相导入
|
|
150
|
+
|
|
151
|
+
**错误结构**:
|
|
152
|
+
```
|
|
153
|
+
service/ → imports → repository/
|
|
154
|
+
repository/ → imports → service/ // 循环!
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**解决**:在 service 定义接口,repository 实现
|
|
158
|
+
```go
|
|
159
|
+
// service/service.go
|
|
160
|
+
type EntityRepository interface {
|
|
161
|
+
FindByID(id uint) (*models.Entity, error)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// repository/repository.go
|
|
165
|
+
// 实现 service.EntityRepository 接口,但不导入 service 包
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 前端陷阱
|
|
171
|
+
|
|
172
|
+
### G8: Modal destroyOnClose 无效
|
|
173
|
+
|
|
174
|
+
**症状**:Modal 关闭后再打开,表单数据残留
|
|
175
|
+
|
|
176
|
+
**原因**:Ant Design v6 API 变更
|
|
177
|
+
|
|
178
|
+
**错误代码**:
|
|
179
|
+
```tsx
|
|
180
|
+
<Modal destroyOnClose> // v6 已废弃
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**正确代码**:
|
|
184
|
+
```tsx
|
|
185
|
+
<Modal destroyOnHidden> // v6 使用这个
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
### G9: 翻译不生效
|
|
191
|
+
|
|
192
|
+
**症状**:`t('key')` 返回 key 本身
|
|
193
|
+
|
|
194
|
+
**原因**:
|
|
195
|
+
1. 未注册翻译文件
|
|
196
|
+
2. 命名空间错误
|
|
197
|
+
|
|
198
|
+
**检查**:
|
|
199
|
+
```typescript
|
|
200
|
+
// index.ts 中确保导入翻译文件
|
|
201
|
+
import './locales/zh-CN/module.json'
|
|
202
|
+
import './locales/en-US/module.json'
|
|
203
|
+
|
|
204
|
+
// 使用时指定正确命名空间
|
|
205
|
+
const { t } = useTranslation('module') // 不是 'common'
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### G10: API 响应解构错误
|
|
211
|
+
|
|
212
|
+
**症状**:`TypeError: Cannot read property 'xxx' of undefined`
|
|
213
|
+
|
|
214
|
+
**原因**:响应结构是 `{ data: { data: {...} } }`
|
|
215
|
+
|
|
216
|
+
**错误代码**:
|
|
217
|
+
```typescript
|
|
218
|
+
const response = await api.get<ApiResponse<Entity>>('/entity/1')
|
|
219
|
+
return response.data // 这是外层 data
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**正确代码**:
|
|
223
|
+
```typescript
|
|
224
|
+
const { data } = await api.get<ApiResponse<Entity>>('/entity/1')
|
|
225
|
+
return data.data // 这是内层 data
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### G11: Table columns 类型错误
|
|
231
|
+
|
|
232
|
+
**症状**:TypeScript 报类型不兼容
|
|
233
|
+
|
|
234
|
+
**原因**:未使用 `ColumnsType<T>`
|
|
235
|
+
|
|
236
|
+
**错误代码**:
|
|
237
|
+
```tsx
|
|
238
|
+
const columns = [
|
|
239
|
+
{ title: 'Name', dataIndex: 'name' }
|
|
240
|
+
]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**正确代码**:
|
|
244
|
+
```tsx
|
|
245
|
+
import type { ColumnsType } from 'antd/es/table'
|
|
246
|
+
|
|
247
|
+
const columns: ColumnsType<Entity> = [
|
|
248
|
+
{ title: 'Name', dataIndex: 'name', key: 'name' }
|
|
249
|
+
]
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### G12: Form 验证不触发
|
|
255
|
+
|
|
256
|
+
**症状**:点击提交按钮无反应
|
|
257
|
+
|
|
258
|
+
**原因**:未正确使用 `Form.useForm()` 或 `validateFields()`
|
|
259
|
+
|
|
260
|
+
**错误代码**:
|
|
261
|
+
```tsx
|
|
262
|
+
const handleSubmit = () => {
|
|
263
|
+
const values = form.getFieldsValue() // 不会触发验证
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**正确代码**:
|
|
268
|
+
```tsx
|
|
269
|
+
const handleSubmit = async () => {
|
|
270
|
+
try {
|
|
271
|
+
const values = await form.validateFields() // 会触发验证
|
|
272
|
+
// 处理提交
|
|
273
|
+
} catch {
|
|
274
|
+
return // 验证失败,不提交
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
### G13: useCallback 依赖缺失
|
|
282
|
+
|
|
283
|
+
**症状**:函数行为不符合预期,使用旧闭包值
|
|
284
|
+
|
|
285
|
+
**原因**:useCallback 依赖数组不完整
|
|
286
|
+
|
|
287
|
+
**错误代码**:
|
|
288
|
+
```tsx
|
|
289
|
+
const fetchData = useCallback(async () => {
|
|
290
|
+
const data = await listItems({ status: filter }) // filter 可能是旧值
|
|
291
|
+
}, []) // 缺少 filter 依赖
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**正确代码**:
|
|
295
|
+
```tsx
|
|
296
|
+
const fetchData = useCallback(async () => {
|
|
297
|
+
const data = await listItems({ status: filter })
|
|
298
|
+
}, [filter]) // 添加 filter 依赖
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
### G14: 权限菜单不显示
|
|
304
|
+
|
|
305
|
+
**症状**:有权限但菜单不出现
|
|
306
|
+
|
|
307
|
+
**原因**:
|
|
308
|
+
1. `app.config.ts` 未启用模块
|
|
309
|
+
2. 权限码不匹配
|
|
310
|
+
|
|
311
|
+
**检查**:
|
|
312
|
+
```typescript
|
|
313
|
+
// routes.tsx
|
|
314
|
+
handle: {
|
|
315
|
+
menu: {
|
|
316
|
+
permission: 'module:entity:view', // 确保格式正确
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// 后端权限注册
|
|
321
|
+
reg.CreateActionI18n(
|
|
322
|
+
"module:entity:view", // 必须与前端一致
|
|
323
|
+
...
|
|
324
|
+
)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## 性能陷阱
|
|
330
|
+
|
|
331
|
+
### G15: N+1 查询
|
|
332
|
+
|
|
333
|
+
**症状**:列表页加载慢,日志显示大量 SQL
|
|
334
|
+
|
|
335
|
+
**原因**:循环中查询关联数据
|
|
336
|
+
|
|
337
|
+
**错误代码**:
|
|
338
|
+
```go
|
|
339
|
+
entities, _ := repo.List(ctx, tenantID)
|
|
340
|
+
for i := range entities {
|
|
341
|
+
entities[i].Category, _ = repo.GetCategory(entities[i].CategoryID) // N次查询
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**正确代码**:
|
|
346
|
+
```go
|
|
347
|
+
// 使用 Preload
|
|
348
|
+
r.db.Preload("Category").Find(&entities)
|
|
349
|
+
|
|
350
|
+
// 或批量查询
|
|
351
|
+
categoryIDs := extractIDs(entities)
|
|
352
|
+
categories, _ := repo.GetCategoriesByIDs(categoryIDs)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
### G16: 无限重渲染
|
|
358
|
+
|
|
359
|
+
**症状**:页面卡死,控制台显示大量渲染日志
|
|
360
|
+
|
|
361
|
+
**原因**:useEffect 依赖数组包含每次渲染都变化的值
|
|
362
|
+
|
|
363
|
+
**错误代码**:
|
|
364
|
+
```tsx
|
|
365
|
+
useEffect(() => {
|
|
366
|
+
fetchData()
|
|
367
|
+
}, [{ page, pageSize }]) // 对象每次都是新引用
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**正确代码**:
|
|
371
|
+
```tsx
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
fetchData()
|
|
374
|
+
}, [page, pageSize]) // 使用原始值
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## 快速排查表
|
|
380
|
+
|
|
381
|
+
| 现象 | 首先检查 |
|
|
382
|
+
|------|----------|
|
|
383
|
+
| 404 Not Found | config.yaml + manifest.go 注册 |
|
|
384
|
+
| 菜单不显示 | app.config.ts 启用 |
|
|
385
|
+
| 翻译键原样显示 | RegisterI18n() + JSON 文件路径 |
|
|
386
|
+
| nil pointer panic | Handler nil 检查 |
|
|
387
|
+
| 数据泄露 | 租户 ID 过滤 |
|
|
388
|
+
| TypeScript 报错 | 类型定义 + ColumnsType |
|
|
389
|
+
| 表单验证无效 | validateFields() 使用 |
|
|
390
|
+
| Modal 数据残留 | destroyOnHidden |
|