@robsun/create-keystone-app 0.1.18 → 0.2.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.
Files changed (44) hide show
  1. package/README.md +4 -5
  2. package/bin/create-keystone-app.js +1 -80
  3. package/package.json +1 -1
  4. package/template/README.md +5 -13
  5. package/template/apps/server/config.example.yaml +0 -1
  6. package/template/apps/server/config.yaml +0 -1
  7. package/template/apps/server/internal/modules/example/api/handler/item_handler.go +162 -0
  8. package/template/apps/server/internal/modules/example/bootstrap/migrations/item.go +21 -0
  9. package/template/apps/server/internal/modules/example/bootstrap/seeds/item.go +33 -0
  10. package/template/apps/server/internal/modules/example/domain/models/item.go +30 -0
  11. package/template/apps/server/internal/modules/{demo → example}/domain/service/errors.go +1 -1
  12. package/template/apps/server/internal/modules/example/domain/service/item_service.go +110 -0
  13. package/template/apps/server/internal/modules/example/infra/repository/item_repository.go +49 -0
  14. package/template/apps/server/internal/modules/example/module.go +55 -17
  15. package/template/apps/server/internal/modules/manifest.go +1 -3
  16. package/template/apps/web/src/app.config.ts +1 -1
  17. package/template/apps/web/src/main.tsx +0 -1
  18. package/template/apps/web/src/modules/example/help/faq.md +23 -0
  19. package/template/apps/web/src/modules/example/help/items.md +26 -0
  20. package/template/apps/web/src/modules/example/help/overview.md +18 -4
  21. package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +227 -0
  22. package/template/apps/web/src/modules/example/routes.tsx +33 -10
  23. package/template/apps/web/src/modules/example/services/exampleItems.ts +32 -0
  24. package/template/apps/web/src/modules/example/types.ts +10 -0
  25. package/template/docs/CONVENTIONS.md +44 -0
  26. package/template/docs/GETTING_STARTED.md +54 -0
  27. package/template/package.json +1 -1
  28. package/template/scripts/check-modules.js +7 -1
  29. package/template/apps/server/internal/modules/demo/api/handler/task_handler.go +0 -152
  30. package/template/apps/server/internal/modules/demo/bootstrap/migrations/task.go +0 -21
  31. package/template/apps/server/internal/modules/demo/bootstrap/seeds/task.go +0 -33
  32. package/template/apps/server/internal/modules/demo/domain/models/task.go +0 -30
  33. package/template/apps/server/internal/modules/demo/domain/service/task_service.go +0 -95
  34. package/template/apps/server/internal/modules/demo/infra/repository/task_repository.go +0 -49
  35. package/template/apps/server/internal/modules/demo/module.go +0 -91
  36. package/template/apps/server/internal/modules/example/handlers.go +0 -19
  37. package/template/apps/web/src/modules/demo/help/overview.md +0 -12
  38. package/template/apps/web/src/modules/demo/index.ts +0 -7
  39. package/template/apps/web/src/modules/demo/pages/DemoTasksPage.tsx +0 -185
  40. package/template/apps/web/src/modules/demo/routes.tsx +0 -43
  41. package/template/apps/web/src/modules/demo/services/demoTasks.ts +0 -28
  42. package/template/apps/web/src/modules/demo/types.ts +0 -9
  43. package/template/apps/web/src/modules/example/pages/ExamplePage.tsx +0 -41
  44. package/template/apps/web/src/modules/example/services/api.ts +0 -8
@@ -6,48 +6,86 @@ import (
6
6
 
7
7
  "github.com/robsuncn/keystone/domain/permissions"
8
8
  "github.com/robsuncn/keystone/infra/jobs"
9
+
10
+ examplehandler "__APP_NAME__/apps/server/internal/modules/example/api/handler"
11
+ examplemigrations "__APP_NAME__/apps/server/internal/modules/example/bootstrap/migrations"
12
+ exampleseeds "__APP_NAME__/apps/server/internal/modules/example/bootstrap/seeds"
13
+ examplemodels "__APP_NAME__/apps/server/internal/modules/example/domain/models"
14
+ exampleservice "__APP_NAME__/apps/server/internal/modules/example/domain/service"
15
+ examplerepository "__APP_NAME__/apps/server/internal/modules/example/infra/repository"
9
16
  )
10
17
 
11
- type Module struct{}
18
+ type Module struct {
19
+ items *exampleservice.ItemService
20
+ }
12
21
 
13
- func NewModule() Module {
14
- return Module{}
22
+ func NewModule() *Module {
23
+ return &Module{}
15
24
  }
16
25
 
17
- func (Module) Name() string {
26
+ func (m *Module) Name() string {
18
27
  return "example"
19
28
  }
20
29
 
21
- func (Module) RegisterRoutes(rg *gin.RouterGroup) {
22
- if rg == nil {
30
+ func (m *Module) RegisterRoutes(rg *gin.RouterGroup) {
31
+ if rg == nil || m == nil {
32
+ return
33
+ }
34
+ handler := examplehandler.NewItemHandler(m.items)
35
+ if handler == nil {
23
36
  return
24
37
  }
25
38
  group := rg.Group("/example")
26
- group.GET("/hello", handleHello)
39
+ group.GET("/items", handler.List)
40
+ group.POST("/items", handler.Create)
41
+ group.PATCH("/items/:id", handler.Update)
42
+ group.DELETE("/items/:id", handler.Delete)
27
43
  }
28
44
 
29
- func (Module) RegisterModels() []interface{} {
30
- return nil
45
+ func (m *Module) RegisterModels() []interface{} {
46
+ return []interface{}{&examplemodels.ExampleItem{}}
31
47
  }
32
48
 
33
- func (Module) RegisterPermissions(reg *permissions.Registry) error {
49
+ func (m *Module) RegisterPermissions(reg *permissions.Registry) error {
34
50
  if reg == nil {
35
51
  return nil
36
52
  }
37
- if err := reg.CreateMenu("example:main", "Example", "example", 100); err != nil {
53
+ if err := reg.CreateMenu("example:item", "Example Items", "example", 10); err != nil {
38
54
  return err
39
55
  }
40
- return reg.CreateAction("example:view", "View Example", "example", "example:main")
56
+ if err := reg.CreateAction("example:item:view", "View Items", "example", "example:item"); err != nil {
57
+ return err
58
+ }
59
+ if err := reg.CreateAction("example:item:manage", "Manage Items", "example", "example:item"); err != nil {
60
+ return err
61
+ }
62
+ return nil
41
63
  }
42
64
 
43
- func (Module) RegisterJobs(_ *jobs.Registry) error {
65
+ func (m *Module) RegisterJobs(_ *jobs.Registry) error {
44
66
  return nil
45
67
  }
46
68
 
47
- func (Module) Migrate(_ *gorm.DB) error {
48
- return nil
69
+ func (m *Module) Migrate(db *gorm.DB) error {
70
+ if db == nil {
71
+ return nil
72
+ }
73
+ m.ensureServices(db)
74
+ return examplemigrations.Migrate(db)
49
75
  }
50
76
 
51
- func (Module) Seed(_ *gorm.DB) error {
52
- return nil
77
+ func (m *Module) Seed(db *gorm.DB) error {
78
+ if db == nil {
79
+ return nil
80
+ }
81
+ m.ensureServices(db)
82
+ return exampleseeds.Seed(db)
83
+ }
84
+
85
+ func (m *Module) ensureServices(db *gorm.DB) {
86
+ if m == nil || db == nil || m.items != nil {
87
+ return
88
+ }
89
+ repo := examplerepository.NewItemRepository(db)
90
+ m.items = exampleservice.NewItemService(repo)
53
91
  }
@@ -1,13 +1,11 @@
1
1
  package modules
2
2
 
3
3
  import (
4
- demo "__APP_NAME__/apps/server/internal/modules/demo"
5
- example "__APP_NAME__/apps/server/internal/modules/example"
4
+ example "__APP_NAME__/apps/server/internal/modules/example"
6
5
  )
7
6
 
8
7
  // RegisterAll wires the module registry for this app.
9
8
  func RegisterAll() {
10
9
  Clear()
11
10
  Register(example.NewModule())
12
- Register(demo.NewModule())
13
11
  }
@@ -8,7 +8,7 @@ export const appConfig: Partial<KeystoneWebConfig> = {
8
8
  platformName: 'Keystone Platform',
9
9
  },
10
10
  modules: {
11
- enabled: ['keystone', 'example', 'demo'],
11
+ enabled: ['keystone', 'example'],
12
12
  },
13
13
  approval: {
14
14
  businessTypes: [{ value: 'general', label: 'General Flow' }],
@@ -6,7 +6,6 @@ import { appConfig } from './app.config'
6
6
  import '@robsun/keystone-web-core/styles/keystone.css'
7
7
  import './index.css'
8
8
  import './modules/example'
9
- import './modules/demo'
10
9
 
11
10
  setKeystoneConfig(appConfig)
12
11
  const dayjsLocale = getKeystoneConfig().ui?.locale?.dayjs ?? 'en'
@@ -0,0 +1,23 @@
1
+ ---
2
+ helpKey: "example/faq"
3
+ title: "Example Module FAQ"
4
+ description: "Common questions for the Example module."
5
+ category: "example"
6
+ tags: ["example", "faq"]
7
+ ---
8
+
9
+ # FAQ
10
+
11
+ ## 为什么列表为空?
12
+ - 确认已运行迁移与种子(首次启动会自动执行)。
13
+ - 检查当前用户是否有 `example:item:view` 权限。
14
+ - 确认 `modules.enabled` 中已启用 `example`。
15
+
16
+ ## 为什么保存失败?
17
+ - Title 必填,且不能为空字符串。
18
+ - 状态仅支持 `active` / `inactive`。
19
+ - 查看后端日志获取更详细错误信息。
20
+
21
+ ## 为什么前后端权限不一致?
22
+ - 检查 `routes.tsx` 中的 `handle.permission`。
23
+ - 检查 `RegisterPermissions` 中的权限注册是否一致。
@@ -0,0 +1,26 @@
1
+ ---
2
+ helpKey: "example/items"
3
+ title: "Example Items"
4
+ description: "Manage example items with full CRUD operations."
5
+ category: "example"
6
+ tags: ["example", "items", "crud"]
7
+ ---
8
+
9
+ # Example Items
10
+
11
+ 该页面展示 Example 模块的完整 CRUD 流程,适合用来对照学习:
12
+
13
+ ## 字段说明
14
+ - **Title**:必填,标题信息。
15
+ - **Description**:可选,简短描述。
16
+ - **Status**:`active` / `inactive`。
17
+
18
+ ## 权限建议
19
+ - `example:item:view`:访问列表与详情。
20
+ - `example:item:manage`:创建、编辑、删除。
21
+
22
+ ## API 端点
23
+ - `GET /api/v1/example/items`
24
+ - `POST /api/v1/example/items`
25
+ - `PATCH /api/v1/example/items/:id`
26
+ - `DELETE /api/v1/example/items/:id`
@@ -1,11 +1,25 @@
1
1
  ---
2
2
  helpKey: "example/overview"
3
- title: "Example Module"
4
- description: "Quick tour of the example module."
3
+ title: "Example Module Overview"
4
+ description: "Overview of the Example module and its full CRUD flow."
5
5
  category: "example"
6
- tags: ["example", "starter"]
6
+ tags: ["example", "items", "crud"]
7
7
  ---
8
8
 
9
9
  # Example Module
10
10
 
11
- Use this page to see how routes, permissions, and API calls connect together.
11
+ Example 模块演示完整的 CRUD、权限命名与前后端分层结构,适合作为新模块开发参考。
12
+
13
+ ## 你能看到什么
14
+ - 前端模块注册(`index.ts`)与路由配置(`routes.tsx`)
15
+ - CRUD 页面与 API 调用(`pages/` + `services/`)
16
+ - 后端 DDD 分层(`domain/` + `infra/` + `bootstrap/`)
17
+ - 权限注册与菜单同步(`RegisterPermissions`)
18
+
19
+ ## 关键文件
20
+ - 前端:`apps/web/src/modules/example/`
21
+ - 后端:`apps/server/internal/modules/example/`
22
+
23
+ ## API 与权限
24
+ - API:`/api/v1/example/items`
25
+ - 权限:`example:item:view`、`example:item:manage`
@@ -0,0 +1,227 @@
1
+ import { useCallback, useEffect, useMemo, useState } from 'react'
2
+ import { App, Button, Card, Form, Input, Modal, Popconfirm, Select, Space, Table, Tag, Typography } from 'antd'
3
+ import type { ColumnsType } from 'antd/es/table'
4
+ import dayjs from 'dayjs'
5
+ import {
6
+ createExampleItem,
7
+ deleteExampleItem,
8
+ listExampleItems,
9
+ updateExampleItem,
10
+ } from '../services/exampleItems'
11
+ import type { ExampleItem, ExampleItemStatus } from '../types'
12
+
13
+ type ExampleItemFormValues = {
14
+ title: string
15
+ description?: string
16
+ status: ExampleItemStatus
17
+ }
18
+
19
+ const statusMeta: Record<ExampleItemStatus, { label: string; color: string }> = {
20
+ active: { label: 'Active', color: 'success' },
21
+ inactive: { label: 'Inactive', color: 'default' },
22
+ }
23
+
24
+ const statusOptions = [
25
+ { value: 'active', label: 'Active' },
26
+ { value: 'inactive', label: 'Inactive' },
27
+ ]
28
+
29
+ export function ExampleItemsPage() {
30
+ const { message } = App.useApp()
31
+ const [items, setItems] = useState<ExampleItem[]>([])
32
+ const [loading, setLoading] = useState(false)
33
+ const [modalOpen, setModalOpen] = useState(false)
34
+ const [saving, setSaving] = useState(false)
35
+ const [editingItem, setEditingItem] = useState<ExampleItem | null>(null)
36
+ const [form] = Form.useForm<ExampleItemFormValues>()
37
+
38
+ const fetchItems = useCallback(async () => {
39
+ setLoading(true)
40
+ try {
41
+ const data = await listExampleItems()
42
+ setItems(data)
43
+ } catch (err) {
44
+ const detail = err instanceof Error ? err.message : 'Failed to load items'
45
+ message.error(detail)
46
+ } finally {
47
+ setLoading(false)
48
+ }
49
+ }, [message])
50
+
51
+ useEffect(() => {
52
+ void fetchItems()
53
+ }, [fetchItems])
54
+
55
+ const openCreate = useCallback(() => {
56
+ setEditingItem(null)
57
+ form.resetFields()
58
+ form.setFieldsValue({ status: 'active' })
59
+ setModalOpen(true)
60
+ }, [form])
61
+
62
+ const openEdit = useCallback(
63
+ (item: ExampleItem) => {
64
+ setEditingItem(item)
65
+ form.setFieldsValue({
66
+ title: item.title,
67
+ description: item.description,
68
+ status: item.status,
69
+ })
70
+ setModalOpen(true)
71
+ },
72
+ [form]
73
+ )
74
+
75
+ const closeModal = useCallback(() => {
76
+ setModalOpen(false)
77
+ setEditingItem(null)
78
+ form.resetFields()
79
+ }, [form])
80
+
81
+ const handleSubmit = useCallback(async () => {
82
+ let values: ExampleItemFormValues
83
+ try {
84
+ values = await form.validateFields()
85
+ } catch {
86
+ return
87
+ }
88
+
89
+ const payload = {
90
+ title: values.title.trim(),
91
+ description: values.description?.trim() ?? '',
92
+ status: values.status,
93
+ }
94
+
95
+ setSaving(true)
96
+ try {
97
+ if (editingItem) {
98
+ await updateExampleItem(editingItem.id, payload)
99
+ message.success('Item updated')
100
+ } else {
101
+ await createExampleItem(payload)
102
+ message.success('Item created')
103
+ }
104
+ closeModal()
105
+ await fetchItems()
106
+ } catch (err) {
107
+ const detail = err instanceof Error ? err.message : 'Failed to save item'
108
+ message.error(detail)
109
+ } finally {
110
+ setSaving(false)
111
+ }
112
+ }, [closeModal, editingItem, fetchItems, form, message])
113
+
114
+ const handleDelete = useCallback(
115
+ async (id: number) => {
116
+ try {
117
+ await deleteExampleItem(id)
118
+ await fetchItems()
119
+ message.success('Item deleted')
120
+ } catch (err) {
121
+ const detail = err instanceof Error ? err.message : 'Failed to delete item'
122
+ message.error(detail)
123
+ }
124
+ },
125
+ [fetchItems, message]
126
+ )
127
+
128
+ const columns: ColumnsType<ExampleItem> = useMemo(
129
+ () => [
130
+ { title: 'Title', dataIndex: 'title', key: 'title' },
131
+ {
132
+ title: 'Description',
133
+ dataIndex: 'description',
134
+ key: 'description',
135
+ render: (value: string) =>
136
+ value ? <Typography.Text type="secondary">{value}</Typography.Text> : '-',
137
+ },
138
+ {
139
+ title: 'Status',
140
+ dataIndex: 'status',
141
+ key: 'status',
142
+ render: (value: ExampleItemStatus) => {
143
+ const meta = statusMeta[value]
144
+ return <Tag color={meta.color}>{meta.label}</Tag>
145
+ },
146
+ },
147
+ {
148
+ title: 'Updated',
149
+ dataIndex: 'updated_at',
150
+ key: 'updated_at',
151
+ render: (value: string) => (value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '-'),
152
+ },
153
+ {
154
+ title: 'Actions',
155
+ key: 'actions',
156
+ render: (_, record) => (
157
+ <Space>
158
+ <Button type="link" onClick={() => openEdit(record)}>
159
+ Edit
160
+ </Button>
161
+ <Popconfirm title="Delete this item?" onConfirm={() => handleDelete(record.id)}>
162
+ <Button type="link" danger>
163
+ Delete
164
+ </Button>
165
+ </Popconfirm>
166
+ </Space>
167
+ ),
168
+ },
169
+ ],
170
+ [handleDelete, openEdit]
171
+ )
172
+
173
+ return (
174
+ <Card
175
+ title="Example Items"
176
+ extra={
177
+ <Space>
178
+ <Button onClick={fetchItems} loading={loading}>
179
+ Refresh
180
+ </Button>
181
+ <Button type="primary" onClick={openCreate}>
182
+ New Item
183
+ </Button>
184
+ </Space>
185
+ }
186
+ >
187
+ <Space direction="vertical" size="middle" style={{ width: '100%' }}>
188
+ <Typography.Text type="secondary">
189
+ This page demonstrates a full CRUD workflow wired to the Example module API.
190
+ </Typography.Text>
191
+ <Table<ExampleItem>
192
+ rowKey="id"
193
+ loading={loading}
194
+ columns={columns}
195
+ dataSource={items}
196
+ pagination={false}
197
+ />
198
+ </Space>
199
+
200
+ <Modal
201
+ title={editingItem ? 'Edit Item' : 'New Item'}
202
+ open={modalOpen}
203
+ onCancel={closeModal}
204
+ onOk={handleSubmit}
205
+ confirmLoading={saving}
206
+ okText={editingItem ? 'Save' : 'Create'}
207
+ destroyOnClose
208
+ >
209
+ <Form form={form} layout="vertical" initialValues={{ status: 'active' }}>
210
+ <Form.Item
211
+ label="Title"
212
+ name="title"
213
+ rules={[{ required: true, whitespace: true, message: 'Title is required' }]}
214
+ >
215
+ <Input placeholder="Item title" allowClear />
216
+ </Form.Item>
217
+ <Form.Item label="Description" name="description">
218
+ <Input.TextArea rows={3} placeholder="Short description" />
219
+ </Form.Item>
220
+ <Form.Item label="Status" name="status" rules={[{ required: true, message: 'Select status' }]}>
221
+ <Select options={statusOptions} />
222
+ </Form.Item>
223
+ </Form>
224
+ </Modal>
225
+ </Card>
226
+ )
227
+ }
@@ -1,20 +1,43 @@
1
- import { lazy } from 'react'
1
+ import { lazy, Suspense, type ComponentType, type ReactElement } from 'react'
2
2
  import type { RouteObject } from 'react-router-dom'
3
- import { QuestionCircleOutlined } from '@ant-design/icons'
3
+ import { AppstoreOutlined } from '@ant-design/icons'
4
+ import { Spin } from 'antd'
4
5
 
5
- const ExamplePage = lazy(() =>
6
- import('./pages/ExamplePage').then((m) => ({ default: m.ExamplePage }))
6
+ const lazyNamed = <T extends Record<string, ComponentType>, K extends keyof T>(
7
+ factory: () => Promise<T>,
8
+ name: K
9
+ ) =>
10
+ lazy(async () => {
11
+ const module = await factory()
12
+ return { default: module[name] }
13
+ })
14
+
15
+ const withSuspense = (element: ReactElement) => (
16
+ <Suspense
17
+ fallback={
18
+ <div style={{ padding: 24, display: 'flex', justifyContent: 'center' }}>
19
+ <Spin />
20
+ </div>
21
+ }
22
+ >
23
+ {element}
24
+ </Suspense>
7
25
  )
8
26
 
27
+ const ExampleItemsPage = lazyNamed(() => import('./pages/ExampleItemsPage'), 'ExampleItemsPage')
28
+
9
29
  export const exampleRoutes: RouteObject[] = [
10
30
  {
11
31
  path: 'example',
12
- element: <ExamplePage />,
32
+ element: <ExampleItemsPage />,
13
33
  handle: {
14
- menu: { label: 'Example', icon: <QuestionCircleOutlined /> },
15
- breadcrumb: 'Example',
16
- permission: 'example:view',
17
- helpKey: 'example/overview',
34
+ menu: { label: 'Example Items', icon: <AppstoreOutlined />, permission: 'example:item:view' },
35
+ breadcrumb: 'Example Items',
36
+ permission: 'example:item:view',
37
+ helpKey: 'example/items',
18
38
  },
19
39
  },
20
- ]
40
+ ].map((route) => ({
41
+ ...route,
42
+ element: route.element ? withSuspense(route.element) : route.element,
43
+ }))
@@ -0,0 +1,32 @@
1
+ import { api, type ApiResponse } from '@robsun/keystone-web-core'
2
+ import type { ExampleItem, ExampleItemStatus } from '../types'
3
+
4
+ type ItemListResponse = {
5
+ items: ExampleItem[]
6
+ }
7
+
8
+ export const listExampleItems = async () => {
9
+ const { data } = await api.get<ApiResponse<ItemListResponse>>('/example/items')
10
+ return data.data.items
11
+ }
12
+
13
+ export const createExampleItem = async (payload: {
14
+ title: string
15
+ description?: string
16
+ status?: ExampleItemStatus
17
+ }) => {
18
+ const { data } = await api.post<ApiResponse<ExampleItem>>('/example/items', payload)
19
+ return data.data
20
+ }
21
+
22
+ export const updateExampleItem = async (
23
+ id: number,
24
+ payload: { title?: string; description?: string; status?: ExampleItemStatus }
25
+ ) => {
26
+ const { data } = await api.patch<ApiResponse<ExampleItem>>(`/example/items/${id}`, payload)
27
+ return data.data
28
+ }
29
+
30
+ export const deleteExampleItem = async (id: number) => {
31
+ await api.delete<ApiResponse<{ id: number }>>(`/example/items/${id}`)
32
+ }
@@ -0,0 +1,10 @@
1
+ export type ExampleItemStatus = 'active' | 'inactive'
2
+
3
+ export interface ExampleItem {
4
+ id: number
5
+ title: string
6
+ description: string
7
+ status: ExampleItemStatus
8
+ created_at: string
9
+ updated_at: string
10
+ }
@@ -88,6 +88,50 @@ type Module interface {
88
88
  - 在 `apps/server/config.yaml` 的 `modules.enabled` 启用模块。
89
89
  - Web 端 `app.config.ts` 保持同名启用列表。
90
90
 
91
+ ## 权限命名与同步
92
+ - 命名规范:`{module}:{resource}:{action}`,例如:`example:item:view`。
93
+ - 菜单权限通常使用 `{module}:{resource}`,用于菜单与分组,例如:`example:item`。
94
+ - 前端使用 `routes.tsx` 的 `handle.permission` 与 `handle.menu.permission` 控制路由/菜单访问。
95
+ - 后端使用 `RegisterPermissions` 注册同名权限,确保权限种子与管理后台一致。
96
+
97
+ 示例(后端注册):
98
+ ```go
99
+ if err := reg.CreateMenu("example:item", "Example Items", "example", 10); err != nil {
100
+ return err
101
+ }
102
+ if err := reg.CreateAction("example:item:view", "View Items", "example", "example:item"); err != nil {
103
+ return err
104
+ }
105
+ if err := reg.CreateAction("example:item:manage", "Manage Items", "example", "example:item"); err != nil {
106
+ return err
107
+ }
108
+ ```
109
+
110
+ 示例(前端路由):
111
+ ```tsx
112
+ {
113
+ path: 'example',
114
+ element: <ExampleItemsPage />,
115
+ handle: {
116
+ menu: { label: 'Example Items', icon: <AppstoreOutlined />, permission: 'example:item:view' },
117
+ breadcrumb: 'Example Items',
118
+ permission: 'example:item:view',
119
+ helpKey: 'example/items',
120
+ },
121
+ }
122
+ ```
123
+
124
+ 权限同步流程(简化):
125
+ ```
126
+ routes.tsx handle.permission
127
+
128
+ 登录接口返回权限列表(permissions)
129
+
130
+ 前端菜单/路由/按钮显示与否
131
+
132
+ RegisterPermissions 负责定义权限种子与后台管理项
133
+ ```
134
+
91
135
  ## 迁移 / 种子 / 权限
92
136
  - `startup.InitializeDatabase` 统一执行:核心迁移 + 模块迁移 + 种子 + 权限注册。
93
137
  - 模块权限通过 `RegisterPermissions` 注入。
@@ -0,0 +1,54 @@
1
+ # 10 分钟上手 Keystone
2
+
3
+ ## 1) 快速启动(约 2 分钟)
4
+ ```bash
5
+ pnpm install
6
+ pnpm dev
7
+ ```
8
+ - Web 端口:`http://localhost:3000`
9
+ - Server 端口:`http://localhost:8080`
10
+ - 默认账号:`admin` / `Admin123!`(Tenant: `default`)
11
+
12
+ ## 2) 认识项目结构
13
+ - `apps/web/`:React + Vite 壳与业务模块(`src/modules/*`)。
14
+ - `apps/server/`:Go 服务端与模块注册(`internal/modules/*`)。
15
+ - `docs/`:开发规范与约定。
16
+ - `scripts/`:跨平台 dev/build/test/clean。
17
+
18
+ ## 3) 理解 Example 模块(前后端对照)
19
+ - 前端模块:
20
+ - `apps/web/src/modules/example/`
21
+ - 入口:`index.ts`(模块注册)
22
+ - 路由与权限:`routes.tsx`
23
+ - 页面与 API:`pages/` + `services/`
24
+ - 后端模块:
25
+ - `apps/server/internal/modules/example/`
26
+ - 入口:`module.go`
27
+ - API:`api/handler/`
28
+ - 领域:`domain/` + `infra/` + `bootstrap/`
29
+
30
+ ## 4) 创建你的第一个模块(复制 Example)
31
+ 1. 复制前端模块:
32
+ - `apps/web/src/modules/example` → `apps/web/src/modules/orders`
33
+ 2. 修改模块名称与权限:
34
+ - 更新 `routes.tsx`、`help/*.md`、`services/*` 中的路径与权限前缀。
35
+ 3. 注册前端模块:
36
+ - 在 `apps/web/src/main.tsx` 添加 `import './modules/orders'`
37
+ - 在 `apps/web/src/app.config.ts` 的 `modules.enabled` 加入 `orders`
38
+ 4. 复制后端模块:
39
+ - `apps/server/internal/modules/example` → `apps/server/internal/modules/orders`
40
+ 5. 修改后端模块名称:
41
+ - 更新 `module.go` 中的 `Name()` 与权限注册
42
+ 6. 注册后端模块:
43
+ - 在 `apps/server/internal/modules/manifest.go` 注册新模块
44
+ - 在 `apps/server/config.yaml` 的 `modules.enabled` 加入 `orders`
45
+
46
+ ## 5) 常见问题(FAQ)
47
+ **Q: 页面空白或 404?**
48
+ A: 确认 `pnpm dev` 正常启动,`modules.enabled` 前后端一致,且前端已在 `main.tsx` 导入模块。
49
+
50
+ **Q: 权限不足导致按钮不可用?**
51
+ A: 检查后端 `RegisterPermissions` 是否注册对应权限,并确认当前用户拥有权限。
52
+
53
+ **Q: API 报错或返回空列表?**
54
+ A: 检查服务端日志、数据库连接、`apps/server/config.yaml` 是否正确。
@@ -7,7 +7,7 @@
7
7
  "build": "node scripts/build.js",
8
8
  "build:web": "pnpm --filter web build",
9
9
  "check-modules": "node scripts/check-modules.js",
10
- "test": "node scripts/test.js",
10
+ "test": "pnpm check-modules && node scripts/test.js",
11
11
  "lint": "pnpm --filter web lint",
12
12
  "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"",
13
13
  "clean": "node scripts/clean.js",