@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.
@@ -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 |