@robsun/create-keystone-app 0.2.11 → 0.2.13
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/package.json +23 -23
- package/template/.claude/skills/keystone-dev/references/APPROVAL.md +47 -0
- package/template/.claude/skills/keystone-dev/references/CAPABILITIES.md +261 -206
- package/template/.claude/skills/keystone-dev/references/TEMPLATES.md +532 -532
- package/template/.codex/skills/keystone-dev/references/APPROVAL.md +47 -0
- package/template/.codex/skills/keystone-dev/references/CAPABILITIES.md +261 -206
- package/template/.codex/skills/keystone-dev/references/TEMPLATES.md +532 -532
- package/template/apps/server/go.mod +1 -1
- package/template/apps/server/go.sum +2 -2
- package/template/apps/server/internal/modules/example/api/handler/item_handler.go +165 -165
- package/template/apps/server/internal/modules/example/domain/service/errors.go +22 -22
- package/template/apps/server/internal/modules/example/module.go +97 -97
- package/template/apps/web/package.json +52 -52
- package/template/apps/web/src/app.config.ts +24 -24
- package/template/apps/web/src/modules/example/index.ts +10 -10
- package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +237 -237
- package/template/apps/web/src/modules/example/routes.tsx +47 -47
- package/template/docs/CONVENTIONS.md +224 -224
- package/template/package.json +27 -27
|
@@ -1,532 +1,532 @@
|
|
|
1
|
-
# Keystone 代码模板
|
|
2
|
-
|
|
3
|
-
## 前端模板
|
|
4
|
-
|
|
5
|
-
### Ant Design v6 注意事项
|
|
6
|
-
- `Space` 使用 `orientation`,不要用 `direction`。
|
|
7
|
-
- `Modal` 使用 `destroyOnHidden`,不要用 `destroyOnClose`。
|
|
8
|
-
- `Drawer` 使用 `size`,不要用 `width`。
|
|
9
|
-
|
|
10
|
-
### routes.tsx
|
|
11
|
-
```tsx
|
|
12
|
-
import { RouteObject } from 'react-router-dom'
|
|
13
|
-
import { registerModule } from '@robsun/keystone-web-core'
|
|
14
|
-
|
|
15
|
-
const routes: RouteObject[] = [
|
|
16
|
-
{
|
|
17
|
-
path: '{name}',
|
|
18
|
-
handle: {
|
|
19
|
-
menu: { title: '{Title}', icon: 'icon-name' },
|
|
20
|
-
permission: '{name}:{resource}:view',
|
|
21
|
-
helpKey: '{name}/index',
|
|
22
|
-
},
|
|
23
|
-
children: [
|
|
24
|
-
{ index: true, lazy: () => import('./pages/List') },
|
|
25
|
-
{ path: 'create', lazy: () => import('./pages/Form') },
|
|
26
|
-
{ path: ':id', lazy: () => import('./pages/Detail') },
|
|
27
|
-
{ path: ':id/edit', lazy: () => import('./pages/Form') },
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
registerModule({ name: '{name}', routes })
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### List.tsx (ProTable)
|
|
36
|
-
```tsx
|
|
37
|
-
import { useEffect, useState } from 'react'
|
|
38
|
-
import { ProTable } from '@robsun/keystone-web-core'
|
|
39
|
-
import type { PaginatedData } from '@robsun/keystone-web-core'
|
|
40
|
-
import type { {Entity} } from '../types'
|
|
41
|
-
import { list{Entity} } from '../services/api'
|
|
42
|
-
|
|
43
|
-
const INITIAL_DATA: PaginatedData<{Entity}> = {
|
|
44
|
-
items: [],
|
|
45
|
-
total: 0,
|
|
46
|
-
page: 1,
|
|
47
|
-
page_size: 10,
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function Component() {
|
|
51
|
-
const [loading, setLoading] = useState(false)
|
|
52
|
-
const [data, setData] = useState(INITIAL_DATA)
|
|
53
|
-
|
|
54
|
-
const columns = [
|
|
55
|
-
{ title: 'ID', dataIndex: 'id' },
|
|
56
|
-
{ title: '名称', dataIndex: 'name' },
|
|
57
|
-
]
|
|
58
|
-
|
|
59
|
-
const fetchData = async (page = data.page, pageSize = data.page_size) => {
|
|
60
|
-
setLoading(true)
|
|
61
|
-
try {
|
|
62
|
-
const result = await list{Entity}({ page, page_size: pageSize })
|
|
63
|
-
setData(result)
|
|
64
|
-
} finally {
|
|
65
|
-
setLoading(false)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
void fetchData()
|
|
71
|
-
}, [])
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<ProTable
|
|
75
|
-
columns={columns}
|
|
76
|
-
dataSource={data.items}
|
|
77
|
-
rowKey="id"
|
|
78
|
-
loading={loading}
|
|
79
|
-
pagination={{
|
|
80
|
-
current: data.page,
|
|
81
|
-
pageSize: data.page_size,
|
|
82
|
-
total: data.total,
|
|
83
|
-
onChange: (page, pageSize) => {
|
|
84
|
-
void fetchData(page, pageSize)
|
|
85
|
-
},
|
|
86
|
-
}}
|
|
87
|
-
/>
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Form.tsx (ProForm)
|
|
93
|
-
```tsx
|
|
94
|
-
import { ProForm } from '@robsun/keystone-web-core'
|
|
95
|
-
import { useParams, useNavigate } from 'react-router-dom'
|
|
96
|
-
import { create{Entity}, update{Entity} } from '../services/api'
|
|
97
|
-
|
|
98
|
-
export function Component() {
|
|
99
|
-
const { id: rawId } = useParams()
|
|
100
|
-
const navigate = useNavigate()
|
|
101
|
-
const id = Number(rawId)
|
|
102
|
-
const isEdit = Number.isFinite(id)
|
|
103
|
-
|
|
104
|
-
const onSubmit = async (values: any) => {
|
|
105
|
-
if (isEdit) {
|
|
106
|
-
await update{Entity}(id, values)
|
|
107
|
-
} else {
|
|
108
|
-
await create{Entity}(values)
|
|
109
|
-
}
|
|
110
|
-
navigate('/{name}')
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<ProForm onFinish={onSubmit}>
|
|
115
|
-
{/* 表单字段 */}
|
|
116
|
-
</ProForm>
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### services/api.ts
|
|
122
|
-
```typescript
|
|
123
|
-
import { api, type ApiResponse, type PaginatedData, type PaginationParams } from '@robsun/keystone-web-core'
|
|
124
|
-
import type { {Entity} } from '../types'
|
|
125
|
-
|
|
126
|
-
// 注意:使用 api 而非 apiClient,响应格式为 { data: { ... } }
|
|
127
|
-
export const list{Entity} = async (params: PaginationParams = {}) => {
|
|
128
|
-
const { data } = await api.get<ApiResponse<PaginatedData<{Entity}>>>(
|
|
129
|
-
'/{module}/{resources}',
|
|
130
|
-
{ params }
|
|
131
|
-
)
|
|
132
|
-
return data.data
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export const create{Entity} = async (payload: Partial<{Entity}>) => {
|
|
136
|
-
const { data } = await api.post<ApiResponse<{Entity}>>('/{module}/{resources}', payload)
|
|
137
|
-
return data.data
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export const update{Entity} = async (id: number, payload: Partial<{Entity}>) => {
|
|
141
|
-
const { data } = await api.patch<ApiResponse<{Entity}>>(`/{module}/{resources}/${id}`, payload)
|
|
142
|
-
return data.data
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export const delete{Entity} = async (id: number) => {
|
|
146
|
-
await api.delete<ApiResponse<{ id: number }>>(`/{module}/{resources}/${id}`)
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### types.ts
|
|
151
|
-
```typescript
|
|
152
|
-
export interface {Entity} {
|
|
153
|
-
id: string
|
|
154
|
-
name: string
|
|
155
|
-
createdAt: string
|
|
156
|
-
updatedAt: string
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### i18n 翻译文件
|
|
161
|
-
|
|
162
|
-
**locales/zh-CN/{module}.json**
|
|
163
|
-
```json
|
|
164
|
-
{
|
|
165
|
-
"title": "{模块标题}",
|
|
166
|
-
"list": {
|
|
167
|
-
"title": "{实体}列表",
|
|
168
|
-
"empty": "暂无数据",
|
|
169
|
-
"columns": {
|
|
170
|
-
"name": "名称",
|
|
171
|
-
"status": "状态",
|
|
172
|
-
"createdAt": "创建时间"
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
"form": {
|
|
176
|
-
"create": "创建{实体}",
|
|
177
|
-
"edit": "编辑{实体}",
|
|
178
|
-
"fields": {
|
|
179
|
-
"name": "名称",
|
|
180
|
-
"description": "描述"
|
|
181
|
-
},
|
|
182
|
-
"placeholders": {
|
|
183
|
-
"name": "请输入名称"
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
"actions": {
|
|
187
|
-
"create": "新建",
|
|
188
|
-
"edit": "编辑",
|
|
189
|
-
"delete": "删除",
|
|
190
|
-
"save": "保存",
|
|
191
|
-
"cancel": "取消"
|
|
192
|
-
},
|
|
193
|
-
"messages": {
|
|
194
|
-
"createSuccess": "创建成功",
|
|
195
|
-
"updateSuccess": "更新成功",
|
|
196
|
-
"deleteSuccess": "删除成功",
|
|
197
|
-
"deleteConfirm": "确定要删除吗?"
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
**locales/en-US/{module}.json**
|
|
203
|
-
```json
|
|
204
|
-
{
|
|
205
|
-
"title": "{Module Title}",
|
|
206
|
-
"list": {
|
|
207
|
-
"title": "{Entity} List",
|
|
208
|
-
"empty": "No data",
|
|
209
|
-
"columns": {
|
|
210
|
-
"name": "Name",
|
|
211
|
-
"status": "Status",
|
|
212
|
-
"createdAt": "Created At"
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
"form": {
|
|
216
|
-
"create": "Create {Entity}",
|
|
217
|
-
"edit": "Edit {Entity}",
|
|
218
|
-
"fields": {
|
|
219
|
-
"name": "Name",
|
|
220
|
-
"description": "Description"
|
|
221
|
-
},
|
|
222
|
-
"placeholders": {
|
|
223
|
-
"name": "Enter name"
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
"actions": {
|
|
227
|
-
"create": "Create",
|
|
228
|
-
"edit": "Edit",
|
|
229
|
-
"delete": "Delete",
|
|
230
|
-
"save": "Save",
|
|
231
|
-
"cancel": "Cancel"
|
|
232
|
-
},
|
|
233
|
-
"messages": {
|
|
234
|
-
"createSuccess": "Created successfully",
|
|
235
|
-
"updateSuccess": "Updated successfully",
|
|
236
|
-
"deleteSuccess": "Deleted successfully",
|
|
237
|
-
"deleteConfirm": "Are you sure to delete?"
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### 使用翻译的 List.tsx
|
|
243
|
-
```tsx
|
|
244
|
-
import { useTranslation } from 'react-i18next'
|
|
245
|
-
import { ProTable } from '@robsun/keystone-web-core'
|
|
246
|
-
|
|
247
|
-
export function Component() {
|
|
248
|
-
const { t } = useTranslation() // 自动使用当前模块的命名空间
|
|
249
|
-
|
|
250
|
-
const columns = [
|
|
251
|
-
{ title: t('{module}:list.columns.name'), dataIndex: 'name' },
|
|
252
|
-
{ title: t('{module}:list.columns.status'), dataIndex: 'status' },
|
|
253
|
-
{ title: t('{module}:list.columns.createdAt'), dataIndex: 'createdAt' },
|
|
254
|
-
]
|
|
255
|
-
|
|
256
|
-
return (
|
|
257
|
-
<ProTable
|
|
258
|
-
columns={columns}
|
|
259
|
-
// ...
|
|
260
|
-
/>
|
|
261
|
-
)
|
|
262
|
-
}
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
## 后端模板
|
|
266
|
-
|
|
267
|
-
### module.go
|
|
268
|
-
```go
|
|
269
|
-
package {name}
|
|
270
|
-
|
|
271
|
-
import (
|
|
272
|
-
"github.com/gin-gonic/gin"
|
|
273
|
-
"gorm.io/gorm"
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
type Module struct{}
|
|
277
|
-
|
|
278
|
-
func (m *Module) Name() string { return "{name}" }
|
|
279
|
-
|
|
280
|
-
func (m *Module) RegisterRoutes(r *gin.RouterGroup) {
|
|
281
|
-
g := r.Group("/{name}")
|
|
282
|
-
g.GET("", handler.List)
|
|
283
|
-
g.GET("/:id", handler.Detail)
|
|
284
|
-
g.POST("", handler.Create)
|
|
285
|
-
g.PATCH("/:id", handler.Update)
|
|
286
|
-
g.DELETE("/:id", handler.Delete)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
func (m *Module) RegisterPermissions() []Permission {
|
|
290
|
-
return []Permission{
|
|
291
|
-
{Name: "{name}:{resource}:view", Title: "查看{Title}"},
|
|
292
|
-
{Name: "{name}:{resource}:create", Title: "创建{Title}"},
|
|
293
|
-
{Name: "{name}:{resource}:update", Title: "编辑{Title}"},
|
|
294
|
-
{Name: "{name}:{resource}:delete", Title: "删除{Title}"},
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
func (m *Module) Migrate(db *gorm.DB) error {
|
|
299
|
-
return db.AutoMigrate(&models.{Entity}{})
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
func (m *Module) Seed(db *gorm.DB) error { return nil }
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### handler/list.go
|
|
306
|
-
```go
|
|
307
|
-
package handler
|
|
308
|
-
|
|
309
|
-
import (
|
|
310
|
-
"github.com/gin-gonic/gin"
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
func (h *Handler) List(c *gin.Context) {
|
|
314
|
-
page, pageSize := parsePagination(c)
|
|
315
|
-
items, total, err := h.svc.List(c, page, pageSize)
|
|
316
|
-
if err != nil {
|
|
317
|
-
respondError(c, err)
|
|
318
|
-
return
|
|
319
|
-
}
|
|
320
|
-
respondPaginated(c, items, total, page, pageSize)
|
|
321
|
-
}
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
### domain/models/entity.go
|
|
325
|
-
```go
|
|
326
|
-
package models
|
|
327
|
-
|
|
328
|
-
import "time"
|
|
329
|
-
|
|
330
|
-
type {Entity} struct {
|
|
331
|
-
ID string `json:"id" gorm:"primaryKey"`
|
|
332
|
-
Name string `json:"name"`
|
|
333
|
-
CreatedAt time.Time `json:"createdAt"`
|
|
334
|
-
UpdatedAt time.Time `json:"updatedAt"`
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### domain/service/service.go
|
|
339
|
-
```go
|
|
340
|
-
package service
|
|
341
|
-
|
|
342
|
-
type Service struct {
|
|
343
|
-
repo *repository.Repository
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
func NewService(repo *repository.Repository) *Service {
|
|
347
|
-
return &Service{repo: repo}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
func (s *Service) List(ctx context.Context, page, pageSize int) ([]models.{Entity}, int64, error) {
|
|
351
|
-
return s.repo.List(ctx, page, pageSize)
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
func (s *Service) Get(ctx context.Context, id string) (*models.{Entity}, error) {
|
|
355
|
-
return s.repo.Get(ctx, id)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
func (s *Service) Create(ctx context.Context, entity *models.{Entity}) error {
|
|
359
|
-
return s.repo.Create(ctx, entity)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
func (s *Service) Update(ctx context.Context, id string, entity *models.{Entity}) error {
|
|
363
|
-
return s.repo.Update(ctx, id, entity)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
func (s *Service) Delete(ctx context.Context, id string) error {
|
|
367
|
-
return s.repo.Delete(ctx, id)
|
|
368
|
-
}
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
### infra/repository/repo.go
|
|
372
|
-
```go
|
|
373
|
-
package repository
|
|
374
|
-
|
|
375
|
-
import (
|
|
376
|
-
"context"
|
|
377
|
-
"gorm.io/gorm"
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
type Repository struct {
|
|
381
|
-
db *gorm.DB
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
func NewRepository(db *gorm.DB) *Repository {
|
|
385
|
-
return &Repository{db: db}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
func (r *Repository) List(ctx context.Context, page, pageSize int) ([]models.{Entity}, int64, error) {
|
|
389
|
-
var items []models.{Entity}
|
|
390
|
-
var total int64
|
|
391
|
-
|
|
392
|
-
r.db.Model(&models.{Entity}{}).Count(&total)
|
|
393
|
-
r.db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&items)
|
|
394
|
-
|
|
395
|
-
return items, total, nil
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
func (r *Repository) Get(ctx context.Context, id string) (*models.{Entity}, error) {
|
|
399
|
-
var item models.{Entity}
|
|
400
|
-
if err := r.db.First(&item, "id = ?", id).Error; err != nil {
|
|
401
|
-
return nil, err
|
|
402
|
-
}
|
|
403
|
-
return &item, nil
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
func (r *Repository) Create(ctx context.Context, entity *models.{Entity}) error {
|
|
407
|
-
return r.db.Create(entity).Error
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
func (r *Repository) Update(ctx context.Context, id string, entity *models.{Entity}) error {
|
|
411
|
-
return r.db.Model(&models.{Entity}{}).Where("id = ?", id).Updates(entity).Error
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
func (r *Repository) Delete(ctx context.Context, id string) error {
|
|
415
|
-
return r.db.Delete(&models.{Entity}{}, "id = ?", id).Error
|
|
416
|
-
}
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
### i18n/i18n.go
|
|
420
|
-
```go
|
|
421
|
-
package i18n
|
|
422
|
-
|
|
423
|
-
import (
|
|
424
|
-
"embed"
|
|
425
|
-
"github.com/robsuncn/keystone/infra/i18n"
|
|
426
|
-
)
|
|
427
|
-
|
|
428
|
-
//go:embed locales/*.json
|
|
429
|
-
var Translations embed.FS
|
|
430
|
-
|
|
431
|
-
func init() {
|
|
432
|
-
// 注册模块翻译文件
|
|
433
|
-
i18n.MustLoadModuleTranslations("{module}", Translations)
|
|
434
|
-
}
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
### i18n/keys.go
|
|
438
|
-
```go
|
|
439
|
-
package i18n
|
|
440
|
-
|
|
441
|
-
// 翻译键常量定义
|
|
442
|
-
const (
|
|
443
|
-
// 成功消息
|
|
444
|
-
KeyItemCreated = "{module}.item.created"
|
|
445
|
-
KeyItemUpdated = "{module}.item.updated"
|
|
446
|
-
KeyItemDeleted = "{module}.item.deleted"
|
|
447
|
-
|
|
448
|
-
// 错误消息
|
|
449
|
-
KeyItemNotFound = "{module}.item.notFound"
|
|
450
|
-
KeyItemInvalid = "{module}.item.invalid"
|
|
451
|
-
KeyItemAlreadyExists = "{module}.item.alreadyExists"
|
|
452
|
-
|
|
453
|
-
// 验证消息
|
|
454
|
-
KeyNameRequired = "{module}.validation.nameRequired"
|
|
455
|
-
KeyNameTooLong = "{module}.validation.nameTooLong"
|
|
456
|
-
)
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
### i18n/locales/zh-CN.json
|
|
460
|
-
```json
|
|
461
|
-
{
|
|
462
|
-
"{module}": {
|
|
463
|
-
"item": {
|
|
464
|
-
"created": "{实体}创建成功",
|
|
465
|
-
"updated": "{实体}更新成功",
|
|
466
|
-
"deleted": "{实体}删除成功",
|
|
467
|
-
"notFound": "{实体}不存在",
|
|
468
|
-
"invalid": "{实体}数据无效",
|
|
469
|
-
"alreadyExists": "{实体}已存在"
|
|
470
|
-
},
|
|
471
|
-
"validation": {
|
|
472
|
-
"nameRequired": "名称不能为空",
|
|
473
|
-
"nameTooLong": "名称长度不能超过 {{max}} 个字符"
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### i18n/locales/en-US.json
|
|
480
|
-
```json
|
|
481
|
-
{
|
|
482
|
-
"{module}": {
|
|
483
|
-
"item": {
|
|
484
|
-
"created": "{Entity} created successfully",
|
|
485
|
-
"updated": "{Entity} updated successfully",
|
|
486
|
-
"deleted": "{Entity} deleted successfully",
|
|
487
|
-
"notFound": "{Entity} not found",
|
|
488
|
-
"invalid": "Invalid {entity} data",
|
|
489
|
-
"alreadyExists": "{Entity} already exists"
|
|
490
|
-
},
|
|
491
|
-
"validation": {
|
|
492
|
-
"nameRequired": "Name is required",
|
|
493
|
-
"nameTooLong": "Name cannot exceed {{max}} characters"
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
### 使用翻译的 service.go
|
|
500
|
-
```go
|
|
501
|
-
package service
|
|
502
|
-
|
|
503
|
-
import (
|
|
504
|
-
"errors"
|
|
505
|
-
"github.com/gin-gonic/gin"
|
|
506
|
-
"github.com/robsuncn/keystone/infra/i18n"
|
|
507
|
-
modulei18n "app/internal/modules/{module}/i18n"
|
|
508
|
-
)
|
|
509
|
-
|
|
510
|
-
func (s *Service) Get(c *gin.Context, id string) (*models.{Entity}, error) {
|
|
511
|
-
item, err := s.repo.Get(c, id)
|
|
512
|
-
if err != nil {
|
|
513
|
-
// 使用翻译键返回错误
|
|
514
|
-
return nil, errors.New(i18n.T(c, modulei18n.KeyItemNotFound))
|
|
515
|
-
}
|
|
516
|
-
return item, nil
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
func (s *Service) Create(c *gin.Context, entity *models.{Entity}) error {
|
|
520
|
-
if entity.Name == "" {
|
|
521
|
-
return errors.New(i18n.T(c, modulei18n.KeyNameRequired))
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
err := s.repo.Create(c, entity)
|
|
525
|
-
if err != nil {
|
|
526
|
-
return err
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// 可以在日志或审计中使用翻译消息
|
|
530
|
-
return nil
|
|
531
|
-
}
|
|
532
|
-
```
|
|
1
|
+
# Keystone 代码模板
|
|
2
|
+
|
|
3
|
+
## 前端模板
|
|
4
|
+
|
|
5
|
+
### Ant Design v6 注意事项
|
|
6
|
+
- `Space` 使用 `orientation`,不要用 `direction`。
|
|
7
|
+
- `Modal` 使用 `destroyOnHidden`,不要用 `destroyOnClose`。
|
|
8
|
+
- `Drawer` 使用 `size`,不要用 `width`。
|
|
9
|
+
|
|
10
|
+
### routes.tsx
|
|
11
|
+
```tsx
|
|
12
|
+
import { RouteObject } from 'react-router-dom'
|
|
13
|
+
import { registerModule } from '@robsun/keystone-web-core'
|
|
14
|
+
|
|
15
|
+
const routes: RouteObject[] = [
|
|
16
|
+
{
|
|
17
|
+
path: '{name}',
|
|
18
|
+
handle: {
|
|
19
|
+
menu: { title: '{Title}', icon: 'icon-name' },
|
|
20
|
+
permission: '{name}:{resource}:view',
|
|
21
|
+
helpKey: '{name}/index',
|
|
22
|
+
},
|
|
23
|
+
children: [
|
|
24
|
+
{ index: true, lazy: () => import('./pages/List') },
|
|
25
|
+
{ path: 'create', lazy: () => import('./pages/Form') },
|
|
26
|
+
{ path: ':id', lazy: () => import('./pages/Detail') },
|
|
27
|
+
{ path: ':id/edit', lazy: () => import('./pages/Form') },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
registerModule({ name: '{name}', routes })
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### List.tsx (ProTable)
|
|
36
|
+
```tsx
|
|
37
|
+
import { useEffect, useState } from 'react'
|
|
38
|
+
import { ProTable } from '@robsun/keystone-web-core'
|
|
39
|
+
import type { PaginatedData } from '@robsun/keystone-web-core'
|
|
40
|
+
import type { {Entity} } from '../types'
|
|
41
|
+
import { list{Entity} } from '../services/api'
|
|
42
|
+
|
|
43
|
+
const INITIAL_DATA: PaginatedData<{Entity}> = {
|
|
44
|
+
items: [],
|
|
45
|
+
total: 0,
|
|
46
|
+
page: 1,
|
|
47
|
+
page_size: 10,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function Component() {
|
|
51
|
+
const [loading, setLoading] = useState(false)
|
|
52
|
+
const [data, setData] = useState(INITIAL_DATA)
|
|
53
|
+
|
|
54
|
+
const columns = [
|
|
55
|
+
{ title: 'ID', dataIndex: 'id' },
|
|
56
|
+
{ title: '名称', dataIndex: 'name' },
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
const fetchData = async (page = data.page, pageSize = data.page_size) => {
|
|
60
|
+
setLoading(true)
|
|
61
|
+
try {
|
|
62
|
+
const result = await list{Entity}({ page, page_size: pageSize })
|
|
63
|
+
setData(result)
|
|
64
|
+
} finally {
|
|
65
|
+
setLoading(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
void fetchData()
|
|
71
|
+
}, [])
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<ProTable
|
|
75
|
+
columns={columns}
|
|
76
|
+
dataSource={data.items}
|
|
77
|
+
rowKey="id"
|
|
78
|
+
loading={loading}
|
|
79
|
+
pagination={{
|
|
80
|
+
current: data.page,
|
|
81
|
+
pageSize: data.page_size,
|
|
82
|
+
total: data.total,
|
|
83
|
+
onChange: (page, pageSize) => {
|
|
84
|
+
void fetchData(page, pageSize)
|
|
85
|
+
},
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Form.tsx (ProForm)
|
|
93
|
+
```tsx
|
|
94
|
+
import { ProForm } from '@robsun/keystone-web-core'
|
|
95
|
+
import { useParams, useNavigate } from 'react-router-dom'
|
|
96
|
+
import { create{Entity}, update{Entity} } from '../services/api'
|
|
97
|
+
|
|
98
|
+
export function Component() {
|
|
99
|
+
const { id: rawId } = useParams()
|
|
100
|
+
const navigate = useNavigate()
|
|
101
|
+
const id = Number(rawId)
|
|
102
|
+
const isEdit = Number.isFinite(id)
|
|
103
|
+
|
|
104
|
+
const onSubmit = async (values: any) => {
|
|
105
|
+
if (isEdit) {
|
|
106
|
+
await update{Entity}(id, values)
|
|
107
|
+
} else {
|
|
108
|
+
await create{Entity}(values)
|
|
109
|
+
}
|
|
110
|
+
navigate('/{name}')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<ProForm onFinish={onSubmit}>
|
|
115
|
+
{/* 表单字段 */}
|
|
116
|
+
</ProForm>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### services/api.ts
|
|
122
|
+
```typescript
|
|
123
|
+
import { api, type ApiResponse, type PaginatedData, type PaginationParams } from '@robsun/keystone-web-core'
|
|
124
|
+
import type { {Entity} } from '../types'
|
|
125
|
+
|
|
126
|
+
// 注意:使用 api 而非 apiClient,响应格式为 { data: { ... } }
|
|
127
|
+
export const list{Entity} = async (params: PaginationParams = {}) => {
|
|
128
|
+
const { data } = await api.get<ApiResponse<PaginatedData<{Entity}>>>(
|
|
129
|
+
'/{module}/{resources}',
|
|
130
|
+
{ params }
|
|
131
|
+
)
|
|
132
|
+
return data.data
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const create{Entity} = async (payload: Partial<{Entity}>) => {
|
|
136
|
+
const { data } = await api.post<ApiResponse<{Entity}>>('/{module}/{resources}', payload)
|
|
137
|
+
return data.data
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const update{Entity} = async (id: number, payload: Partial<{Entity}>) => {
|
|
141
|
+
const { data } = await api.patch<ApiResponse<{Entity}>>(`/{module}/{resources}/${id}`, payload)
|
|
142
|
+
return data.data
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const delete{Entity} = async (id: number) => {
|
|
146
|
+
await api.delete<ApiResponse<{ id: number }>>(`/{module}/{resources}/${id}`)
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### types.ts
|
|
151
|
+
```typescript
|
|
152
|
+
export interface {Entity} {
|
|
153
|
+
id: string
|
|
154
|
+
name: string
|
|
155
|
+
createdAt: string
|
|
156
|
+
updatedAt: string
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### i18n 翻译文件
|
|
161
|
+
|
|
162
|
+
**locales/zh-CN/{module}.json**
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"title": "{模块标题}",
|
|
166
|
+
"list": {
|
|
167
|
+
"title": "{实体}列表",
|
|
168
|
+
"empty": "暂无数据",
|
|
169
|
+
"columns": {
|
|
170
|
+
"name": "名称",
|
|
171
|
+
"status": "状态",
|
|
172
|
+
"createdAt": "创建时间"
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
"form": {
|
|
176
|
+
"create": "创建{实体}",
|
|
177
|
+
"edit": "编辑{实体}",
|
|
178
|
+
"fields": {
|
|
179
|
+
"name": "名称",
|
|
180
|
+
"description": "描述"
|
|
181
|
+
},
|
|
182
|
+
"placeholders": {
|
|
183
|
+
"name": "请输入名称"
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"actions": {
|
|
187
|
+
"create": "新建",
|
|
188
|
+
"edit": "编辑",
|
|
189
|
+
"delete": "删除",
|
|
190
|
+
"save": "保存",
|
|
191
|
+
"cancel": "取消"
|
|
192
|
+
},
|
|
193
|
+
"messages": {
|
|
194
|
+
"createSuccess": "创建成功",
|
|
195
|
+
"updateSuccess": "更新成功",
|
|
196
|
+
"deleteSuccess": "删除成功",
|
|
197
|
+
"deleteConfirm": "确定要删除吗?"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**locales/en-US/{module}.json**
|
|
203
|
+
```json
|
|
204
|
+
{
|
|
205
|
+
"title": "{Module Title}",
|
|
206
|
+
"list": {
|
|
207
|
+
"title": "{Entity} List",
|
|
208
|
+
"empty": "No data",
|
|
209
|
+
"columns": {
|
|
210
|
+
"name": "Name",
|
|
211
|
+
"status": "Status",
|
|
212
|
+
"createdAt": "Created At"
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
"form": {
|
|
216
|
+
"create": "Create {Entity}",
|
|
217
|
+
"edit": "Edit {Entity}",
|
|
218
|
+
"fields": {
|
|
219
|
+
"name": "Name",
|
|
220
|
+
"description": "Description"
|
|
221
|
+
},
|
|
222
|
+
"placeholders": {
|
|
223
|
+
"name": "Enter name"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"actions": {
|
|
227
|
+
"create": "Create",
|
|
228
|
+
"edit": "Edit",
|
|
229
|
+
"delete": "Delete",
|
|
230
|
+
"save": "Save",
|
|
231
|
+
"cancel": "Cancel"
|
|
232
|
+
},
|
|
233
|
+
"messages": {
|
|
234
|
+
"createSuccess": "Created successfully",
|
|
235
|
+
"updateSuccess": "Updated successfully",
|
|
236
|
+
"deleteSuccess": "Deleted successfully",
|
|
237
|
+
"deleteConfirm": "Are you sure to delete?"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 使用翻译的 List.tsx
|
|
243
|
+
```tsx
|
|
244
|
+
import { useTranslation } from 'react-i18next'
|
|
245
|
+
import { ProTable } from '@robsun/keystone-web-core'
|
|
246
|
+
|
|
247
|
+
export function Component() {
|
|
248
|
+
const { t } = useTranslation() // 自动使用当前模块的命名空间
|
|
249
|
+
|
|
250
|
+
const columns = [
|
|
251
|
+
{ title: t('{module}:list.columns.name'), dataIndex: 'name' },
|
|
252
|
+
{ title: t('{module}:list.columns.status'), dataIndex: 'status' },
|
|
253
|
+
{ title: t('{module}:list.columns.createdAt'), dataIndex: 'createdAt' },
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<ProTable
|
|
258
|
+
columns={columns}
|
|
259
|
+
// ...
|
|
260
|
+
/>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## 后端模板
|
|
266
|
+
|
|
267
|
+
### module.go
|
|
268
|
+
```go
|
|
269
|
+
package {name}
|
|
270
|
+
|
|
271
|
+
import (
|
|
272
|
+
"github.com/gin-gonic/gin"
|
|
273
|
+
"gorm.io/gorm"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
type Module struct{}
|
|
277
|
+
|
|
278
|
+
func (m *Module) Name() string { return "{name}" }
|
|
279
|
+
|
|
280
|
+
func (m *Module) RegisterRoutes(r *gin.RouterGroup) {
|
|
281
|
+
g := r.Group("/{name}")
|
|
282
|
+
g.GET("", handler.List)
|
|
283
|
+
g.GET("/:id", handler.Detail)
|
|
284
|
+
g.POST("", handler.Create)
|
|
285
|
+
g.PATCH("/:id", handler.Update)
|
|
286
|
+
g.DELETE("/:id", handler.Delete)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
func (m *Module) RegisterPermissions() []Permission {
|
|
290
|
+
return []Permission{
|
|
291
|
+
{Name: "{name}:{resource}:view", Title: "查看{Title}"},
|
|
292
|
+
{Name: "{name}:{resource}:create", Title: "创建{Title}"},
|
|
293
|
+
{Name: "{name}:{resource}:update", Title: "编辑{Title}"},
|
|
294
|
+
{Name: "{name}:{resource}:delete", Title: "删除{Title}"},
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
func (m *Module) Migrate(db *gorm.DB) error {
|
|
299
|
+
return db.AutoMigrate(&models.{Entity}{})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
func (m *Module) Seed(db *gorm.DB) error { return nil }
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### handler/list.go
|
|
306
|
+
```go
|
|
307
|
+
package handler
|
|
308
|
+
|
|
309
|
+
import (
|
|
310
|
+
"github.com/gin-gonic/gin"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
func (h *Handler) List(c *gin.Context) {
|
|
314
|
+
page, pageSize := parsePagination(c)
|
|
315
|
+
items, total, err := h.svc.List(c, page, pageSize)
|
|
316
|
+
if err != nil {
|
|
317
|
+
respondError(c, err)
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
respondPaginated(c, items, total, page, pageSize)
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### domain/models/entity.go
|
|
325
|
+
```go
|
|
326
|
+
package models
|
|
327
|
+
|
|
328
|
+
import "time"
|
|
329
|
+
|
|
330
|
+
type {Entity} struct {
|
|
331
|
+
ID string `json:"id" gorm:"primaryKey"`
|
|
332
|
+
Name string `json:"name"`
|
|
333
|
+
CreatedAt time.Time `json:"createdAt"`
|
|
334
|
+
UpdatedAt time.Time `json:"updatedAt"`
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### domain/service/service.go
|
|
339
|
+
```go
|
|
340
|
+
package service
|
|
341
|
+
|
|
342
|
+
type Service struct {
|
|
343
|
+
repo *repository.Repository
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
func NewService(repo *repository.Repository) *Service {
|
|
347
|
+
return &Service{repo: repo}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
func (s *Service) List(ctx context.Context, page, pageSize int) ([]models.{Entity}, int64, error) {
|
|
351
|
+
return s.repo.List(ctx, page, pageSize)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
func (s *Service) Get(ctx context.Context, id string) (*models.{Entity}, error) {
|
|
355
|
+
return s.repo.Get(ctx, id)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
func (s *Service) Create(ctx context.Context, entity *models.{Entity}) error {
|
|
359
|
+
return s.repo.Create(ctx, entity)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
func (s *Service) Update(ctx context.Context, id string, entity *models.{Entity}) error {
|
|
363
|
+
return s.repo.Update(ctx, id, entity)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func (s *Service) Delete(ctx context.Context, id string) error {
|
|
367
|
+
return s.repo.Delete(ctx, id)
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### infra/repository/repo.go
|
|
372
|
+
```go
|
|
373
|
+
package repository
|
|
374
|
+
|
|
375
|
+
import (
|
|
376
|
+
"context"
|
|
377
|
+
"gorm.io/gorm"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
type Repository struct {
|
|
381
|
+
db *gorm.DB
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
func NewRepository(db *gorm.DB) *Repository {
|
|
385
|
+
return &Repository{db: db}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
func (r *Repository) List(ctx context.Context, page, pageSize int) ([]models.{Entity}, int64, error) {
|
|
389
|
+
var items []models.{Entity}
|
|
390
|
+
var total int64
|
|
391
|
+
|
|
392
|
+
r.db.Model(&models.{Entity}{}).Count(&total)
|
|
393
|
+
r.db.Offset((page - 1) * pageSize).Limit(pageSize).Find(&items)
|
|
394
|
+
|
|
395
|
+
return items, total, nil
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
func (r *Repository) Get(ctx context.Context, id string) (*models.{Entity}, error) {
|
|
399
|
+
var item models.{Entity}
|
|
400
|
+
if err := r.db.First(&item, "id = ?", id).Error; err != nil {
|
|
401
|
+
return nil, err
|
|
402
|
+
}
|
|
403
|
+
return &item, nil
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
func (r *Repository) Create(ctx context.Context, entity *models.{Entity}) error {
|
|
407
|
+
return r.db.Create(entity).Error
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
func (r *Repository) Update(ctx context.Context, id string, entity *models.{Entity}) error {
|
|
411
|
+
return r.db.Model(&models.{Entity}{}).Where("id = ?", id).Updates(entity).Error
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
func (r *Repository) Delete(ctx context.Context, id string) error {
|
|
415
|
+
return r.db.Delete(&models.{Entity}{}, "id = ?", id).Error
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### i18n/i18n.go
|
|
420
|
+
```go
|
|
421
|
+
package i18n
|
|
422
|
+
|
|
423
|
+
import (
|
|
424
|
+
"embed"
|
|
425
|
+
"github.com/robsuncn/keystone/infra/i18n"
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
//go:embed locales/*.json
|
|
429
|
+
var Translations embed.FS
|
|
430
|
+
|
|
431
|
+
func init() {
|
|
432
|
+
// 注册模块翻译文件
|
|
433
|
+
i18n.MustLoadModuleTranslations("{module}", Translations)
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### i18n/keys.go
|
|
438
|
+
```go
|
|
439
|
+
package i18n
|
|
440
|
+
|
|
441
|
+
// 翻译键常量定义
|
|
442
|
+
const (
|
|
443
|
+
// 成功消息
|
|
444
|
+
KeyItemCreated = "{module}.item.created"
|
|
445
|
+
KeyItemUpdated = "{module}.item.updated"
|
|
446
|
+
KeyItemDeleted = "{module}.item.deleted"
|
|
447
|
+
|
|
448
|
+
// 错误消息
|
|
449
|
+
KeyItemNotFound = "{module}.item.notFound"
|
|
450
|
+
KeyItemInvalid = "{module}.item.invalid"
|
|
451
|
+
KeyItemAlreadyExists = "{module}.item.alreadyExists"
|
|
452
|
+
|
|
453
|
+
// 验证消息
|
|
454
|
+
KeyNameRequired = "{module}.validation.nameRequired"
|
|
455
|
+
KeyNameTooLong = "{module}.validation.nameTooLong"
|
|
456
|
+
)
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### i18n/locales/zh-CN.json
|
|
460
|
+
```json
|
|
461
|
+
{
|
|
462
|
+
"{module}": {
|
|
463
|
+
"item": {
|
|
464
|
+
"created": "{实体}创建成功",
|
|
465
|
+
"updated": "{实体}更新成功",
|
|
466
|
+
"deleted": "{实体}删除成功",
|
|
467
|
+
"notFound": "{实体}不存在",
|
|
468
|
+
"invalid": "{实体}数据无效",
|
|
469
|
+
"alreadyExists": "{实体}已存在"
|
|
470
|
+
},
|
|
471
|
+
"validation": {
|
|
472
|
+
"nameRequired": "名称不能为空",
|
|
473
|
+
"nameTooLong": "名称长度不能超过 {{max}} 个字符"
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### i18n/locales/en-US.json
|
|
480
|
+
```json
|
|
481
|
+
{
|
|
482
|
+
"{module}": {
|
|
483
|
+
"item": {
|
|
484
|
+
"created": "{Entity} created successfully",
|
|
485
|
+
"updated": "{Entity} updated successfully",
|
|
486
|
+
"deleted": "{Entity} deleted successfully",
|
|
487
|
+
"notFound": "{Entity} not found",
|
|
488
|
+
"invalid": "Invalid {entity} data",
|
|
489
|
+
"alreadyExists": "{Entity} already exists"
|
|
490
|
+
},
|
|
491
|
+
"validation": {
|
|
492
|
+
"nameRequired": "Name is required",
|
|
493
|
+
"nameTooLong": "Name cannot exceed {{max}} characters"
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### 使用翻译的 service.go
|
|
500
|
+
```go
|
|
501
|
+
package service
|
|
502
|
+
|
|
503
|
+
import (
|
|
504
|
+
"errors"
|
|
505
|
+
"github.com/gin-gonic/gin"
|
|
506
|
+
"github.com/robsuncn/keystone/infra/i18n"
|
|
507
|
+
modulei18n "app/internal/modules/{module}/i18n"
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
func (s *Service) Get(c *gin.Context, id string) (*models.{Entity}, error) {
|
|
511
|
+
item, err := s.repo.Get(c, id)
|
|
512
|
+
if err != nil {
|
|
513
|
+
// 使用翻译键返回错误
|
|
514
|
+
return nil, errors.New(i18n.T(c, modulei18n.KeyItemNotFound))
|
|
515
|
+
}
|
|
516
|
+
return item, nil
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
func (s *Service) Create(c *gin.Context, entity *models.{Entity}) error {
|
|
520
|
+
if entity.Name == "" {
|
|
521
|
+
return errors.New(i18n.T(c, modulei18n.KeyNameRequired))
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
err := s.repo.Create(c, entity)
|
|
525
|
+
if err != nil {
|
|
526
|
+
return err
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 可以在日志或审计中使用翻译消息
|
|
530
|
+
return nil
|
|
531
|
+
}
|
|
532
|
+
```
|