@robsun/create-keystone-app 0.2.6 → 0.2.8
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 +2 -2
- package/template/.claude/skills/keystone-dev/SKILL.md +12 -9
- package/template/.claude/skills/keystone-dev/references/APPROVAL.md +74 -0
- package/template/.claude/skills/keystone-dev/{CAPABILITIES.md → references/CAPABILITIES.md} +1 -0
- package/template/.claude/skills/keystone-dev/{TEMPLATES.md → references/TEMPLATES.md} +1 -0
- package/template/.claude/skills/keystone-dev/references/TESTING.md +44 -0
- package/template/.codex/skills/keystone-dev/SKILL.md +12 -9
- package/template/.codex/skills/keystone-dev/references/APPROVAL.md +74 -0
- package/template/.codex/skills/keystone-dev/{CAPABILITIES.md → references/CAPABILITIES.md} +1 -0
- package/template/.codex/skills/keystone-dev/{TEMPLATES.md → references/TEMPLATES.md} +1 -0
- package/template/.codex/skills/keystone-dev/references/TESTING.md +44 -0
- package/template/apps/server/go.mod +1 -1
- package/template/apps/server/go.sum +1 -0
- package/template/apps/server/internal/modules/example/api/handler/item_handler.go +35 -32
- package/template/apps/server/internal/modules/example/domain/service/errors.go +13 -0
- package/template/apps/server/internal/modules/example/i18n/i18n.go +16 -0
- package/template/apps/server/internal/modules/example/i18n/keys.go +23 -0
- package/template/apps/server/internal/modules/example/i18n/locales/en-US.json +18 -0
- package/template/apps/server/internal/modules/example/i18n/locales/zh-CN.json +18 -0
- package/template/apps/server/internal/modules/example/module.go +9 -3
- package/template/apps/web/package.json +3 -1
- package/template/apps/web/src/app.config.ts +8 -1
- package/template/apps/web/src/modules/example/index.ts +7 -1
- package/template/apps/web/src/modules/example/locales/en-US/example.json +32 -0
- package/template/apps/web/src/modules/example/locales/zh-CN/example.json +32 -0
- package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +47 -45
- package/template/apps/web/src/modules/example/routes.tsx +6 -2
- package/template/docs/CONVENTIONS.md +73 -1
- package/template/docs/I18N.md +319 -0
- package/template/package.json +1 -0
- package/template/scripts/generate-i18n-types.js +154 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robsun/create-keystone-app",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"build": "node scripts/build.js",
|
|
6
6
|
"prepublishOnly": "node scripts/build.js && node scripts/prune-template-deps.js"
|
|
@@ -20,4 +20,4 @@
|
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
}
|
|
23
|
-
}
|
|
23
|
+
}
|
|
@@ -10,10 +10,11 @@ description: 基于 Keystone 平台开发业务模块。当用户需要创建新
|
|
|
10
10
|
## 工作流程
|
|
11
11
|
|
|
12
12
|
1. **解析需求** → 识别所需能力
|
|
13
|
-
2. **生成前端模块** → routes.tsx, pages/, services/, types.ts
|
|
14
|
-
3. **生成后端模块** → handler/, service/, repository/, migrations/
|
|
15
|
-
4. **自动注册** → main.tsx + manifest.go + config.yaml
|
|
16
|
-
5. **输出调整建议**
|
|
13
|
+
2. **生成前端模块** → routes.tsx, pages/, services/, types.ts
|
|
14
|
+
3. **生成后端模块** → handler/, service/, repository/, migrations/
|
|
15
|
+
4. **自动注册** → main.tsx + manifest.go + config.yaml
|
|
16
|
+
5. **输出调整建议**
|
|
17
|
+
6. **补测试并执行** → 见 references/testing.md
|
|
17
18
|
|
|
18
19
|
## 能力选择矩阵
|
|
19
20
|
|
|
@@ -26,11 +27,13 @@ description: 基于 Keystone 平台开发业务模块。当用户需要创建新
|
|
|
26
27
|
| 导入、批量 | DataImporter | ImportHandler + Job队列 |
|
|
27
28
|
| 导出、下载 | DataExporter | ExportHandler + Job队列 |
|
|
28
29
|
| 上传、附件 | FileUpload | UploadHandler + 存储服务 |
|
|
29
|
-
| 审批、流程 | ApprovalFlowEditor | 审批引擎 |
|
|
30
|
-
| 权限 | PermissionGuard | 权限中间件 |
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
| 审批、流程 | ApprovalFlowEditor | 审批引擎 |
|
|
31
|
+
| 权限 | PermissionGuard | 权限中间件 |
|
|
32
|
+
|
|
33
|
+
审批流程与接入说明见 [references/approval.md](references/approval.md)。
|
|
34
|
+
测试清单见 [references/testing.md](references/testing.md)。
|
|
35
|
+
详细能力清单见 [references/CAPABILITIES.md](references/CAPABILITIES.md)。
|
|
36
|
+
代码模板见 [references/TEMPLATES.md](references/TEMPLATES.md)。
|
|
34
37
|
|
|
35
38
|
## 模块结构约定
|
|
36
39
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Keystone 审批接入流程
|
|
2
|
+
|
|
3
|
+
## 目标
|
|
4
|
+
本文只描述审批接入所需的表单 schema 与 context 映射等开发工作,
|
|
5
|
+
流程节点与审批路径由人工在审批配置页面完成。
|
|
6
|
+
|
|
7
|
+
## 适用场景
|
|
8
|
+
- 新业务需要审批门禁(新业务类型)。
|
|
9
|
+
- 既有业务接入审批门禁(在业务表单提交或关键业务动作前加审批)。
|
|
10
|
+
|
|
11
|
+
## 不在范围
|
|
12
|
+
- 配置审批流程与节点(由人工在流程配置页面完成)。
|
|
13
|
+
- 修改 approval 基础模块或审批页面本身。
|
|
14
|
+
- 改造审批权限体系或审批路由结构。
|
|
15
|
+
|
|
16
|
+
## 开发路径选择
|
|
17
|
+
- Greenfield:新业务从零引入审批。
|
|
18
|
+
- Retrofit:已有业务接入审批。
|
|
19
|
+
|
|
20
|
+
## 开发步骤 A:新业务(Greenfield)
|
|
21
|
+
1) 前端配置 `approval.businessTypes` 标签映射(用于流程配置时选择表单),
|
|
22
|
+
流程配置页面的业务类型选择会作为 schema code 加载表单字段,需保持一致。
|
|
23
|
+
2) 注册表单 schema(条件节点字段来源)。
|
|
24
|
+
3) 在业务提交处创建审批实例,写入 context JSON 用于条件路由。
|
|
25
|
+
4) 绑定 OnApproved/OnRejected 回调更新业务状态。
|
|
26
|
+
5) 按测试清单执行(见 testing.md)。
|
|
27
|
+
|
|
28
|
+
## 开发步骤 B:已有业务接入(Retrofit)
|
|
29
|
+
1) 确认触发点与审批边界(业务表单提交或关键业务动作前)。
|
|
30
|
+
2) 将现有字段映射为 context JSON(供条件节点判断)。
|
|
31
|
+
3) 做幂等:按业务类型 + 业务 ID 查已有实例再创建。
|
|
32
|
+
4) pending 期间阻断业务动作(状态拦截或硬拦截)。
|
|
33
|
+
5) 决定是否回填存量数据(跳过或补建实例)。
|
|
34
|
+
|
|
35
|
+
## 业务侧改动清单
|
|
36
|
+
|
|
37
|
+
后端(业务模块内)
|
|
38
|
+
- 在业务 service/handler 中调用审批服务创建实例(必要时通过 FlowMatcher 选 flow)。
|
|
39
|
+
- 在业务模块注册审批回调,审批通过/驳回时更新业务状态。
|
|
40
|
+
- 需要数据范围过滤时,实现 ApprovalContextResolver 并在应用装配时注入。
|
|
41
|
+
- 如需默认 flow 或 schema,放在业务模块的迁移/seed 中,不改动审批模块。
|
|
42
|
+
|
|
43
|
+
前端(业务模块内)
|
|
44
|
+
- 配置 `approval.businessTypes` 标签映射,保证业务类型显示正确。
|
|
45
|
+
- 流程配置使用已有审批管理页面,不做二次开发。
|
|
46
|
+
|
|
47
|
+
## 权限说明
|
|
48
|
+
使用系统既有审批权限码:
|
|
49
|
+
approval:flow:view|create|edit|delete
|
|
50
|
+
approval:instance:view|approve|cancel
|
|
51
|
+
approval:record:view|export
|
|
52
|
+
|
|
53
|
+
## 集成自检
|
|
54
|
+
- Flow code <= 50 且使用大写;business type 用短字符串。
|
|
55
|
+
- approve/reject/cancel 使用 version 做乐观锁。
|
|
56
|
+
- 变更接口时更新 OpenAPI 合同。
|
|
57
|
+
- 新增默认流程时补迁移或 seed。
|
|
58
|
+
|
|
59
|
+
测试清单见 [testing.md](testing.md)。
|
|
60
|
+
|
|
61
|
+
## 参考入口
|
|
62
|
+
|
|
63
|
+
后端
|
|
64
|
+
- domain/approval/service/service.go
|
|
65
|
+
- domain/approval/service/flow_matcher.go
|
|
66
|
+
- domain/approval/service/callback_registry.go
|
|
67
|
+
- api/routes/approval_routes.go
|
|
68
|
+
- contracts/011-approvals/approvals-api.yaml
|
|
69
|
+
|
|
70
|
+
前端
|
|
71
|
+
- packages/keystone-web-core/src/platform/config/appConfig.ts
|
|
72
|
+
- packages/keystone-web-core/src/platform/core/approval/services/approvalService.ts
|
|
73
|
+
- packages/keystone-web-core/src/platform/core/approval/stores/approvalStore.ts
|
|
74
|
+
- packages/keystone-web-core/src/modules/keystone/approval/pages/*
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Keystone 开发测试清单
|
|
2
|
+
|
|
3
|
+
## 快速入口
|
|
4
|
+
- 仓库根目录:`pnpm test`(模板测试 + `go test ./...`)
|
|
5
|
+
- 仅 Go:`go test ./...`
|
|
6
|
+
- 模板项目:`pnpm -C packages/create-keystone-app/template test`
|
|
7
|
+
|
|
8
|
+
## 按改动范围选择
|
|
9
|
+
后端(api/domain/infra/bootstrap/server)
|
|
10
|
+
- `go test ./...`
|
|
11
|
+
- 若改动较大可补 `go vet ./...`
|
|
12
|
+
|
|
13
|
+
模板前端(packages/create-keystone-app/template/apps/web)
|
|
14
|
+
- `pnpm -C packages/create-keystone-app/template/apps/web typecheck`
|
|
15
|
+
- `pnpm -C packages/create-keystone-app/template/apps/web lint`
|
|
16
|
+
- `pnpm -C packages/create-keystone-app/template/apps/web test`
|
|
17
|
+
|
|
18
|
+
前端核心包(packages/keystone-web-core)
|
|
19
|
+
- `pnpm -C packages/keystone-web-core build`
|
|
20
|
+
|
|
21
|
+
脚手架/模板变更
|
|
22
|
+
- `pnpm scaffold:verify`
|
|
23
|
+
|
|
24
|
+
合同变更
|
|
25
|
+
- `pnpm contracts:check`
|
|
26
|
+
- 如更新了合同内容,执行 `pnpm contracts:sync`
|
|
27
|
+
|
|
28
|
+
## 写测试(必做)
|
|
29
|
+
后端
|
|
30
|
+
- 领域/服务逻辑新增或改动:在 `tests/unit/platform/service` 或对应 domain 下补单测。
|
|
31
|
+
- Repository 变更:在 `tests/unit/platform/repository` 补单测。
|
|
32
|
+
- 新增/调整 API:在 `tests/integration/platform/api` 补集成测试。
|
|
33
|
+
- 参考用例:`tests/unit/platform/approval/service_test.go`、`tests/integration/platform/api/approval_schema_test.go`。
|
|
34
|
+
|
|
35
|
+
前端(模板 web)
|
|
36
|
+
- 新增页面/交互:在 `packages/create-keystone-app/template/apps/web/tests` 新增 `*.test.tsx`。
|
|
37
|
+
- 使用 vitest + Testing Library(已在 `vite.config.ts` 配好 test 配置)。
|
|
38
|
+
|
|
39
|
+
前端核心包(keystone-web-core)
|
|
40
|
+
- 新增核心组件/逻辑:优先在模板 web 侧写集成测试覆盖。
|
|
41
|
+
- 如必须在包内写单测,需要补齐包内测试配置后再添加用例。
|
|
42
|
+
|
|
43
|
+
## 最小通过标准
|
|
44
|
+
- 单点改动跑对应子项;跨域改动直接跑 `pnpm test`。
|
|
@@ -10,10 +10,11 @@ description: 基于 Keystone 平台开发业务模块。当用户需要创建新
|
|
|
10
10
|
## 工作流程
|
|
11
11
|
|
|
12
12
|
1. **解析需求** → 识别所需能力
|
|
13
|
-
2. **生成前端模块** → routes.tsx, pages/, services/, types.ts
|
|
14
|
-
3. **生成后端模块** → handler/, service/, repository/, migrations/
|
|
15
|
-
4. **自动注册** → main.tsx + manifest.go + config.yaml
|
|
16
|
-
5. **输出调整建议**
|
|
13
|
+
2. **生成前端模块** → routes.tsx, pages/, services/, types.ts
|
|
14
|
+
3. **生成后端模块** → handler/, service/, repository/, migrations/
|
|
15
|
+
4. **自动注册** → main.tsx + manifest.go + config.yaml
|
|
16
|
+
5. **输出调整建议**
|
|
17
|
+
6. **补测试并执行** → 见 references/testing.md
|
|
17
18
|
|
|
18
19
|
## 能力选择矩阵
|
|
19
20
|
|
|
@@ -26,11 +27,13 @@ description: 基于 Keystone 平台开发业务模块。当用户需要创建新
|
|
|
26
27
|
| 导入、批量 | DataImporter | ImportHandler + Job队列 |
|
|
27
28
|
| 导出、下载 | DataExporter | ExportHandler + Job队列 |
|
|
28
29
|
| 上传、附件 | FileUpload | UploadHandler + 存储服务 |
|
|
29
|
-
| 审批、流程 | ApprovalFlowEditor | 审批引擎 |
|
|
30
|
-
| 权限 | PermissionGuard | 权限中间件 |
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
| 审批、流程 | ApprovalFlowEditor | 审批引擎 |
|
|
31
|
+
| 权限 | PermissionGuard | 权限中间件 |
|
|
32
|
+
|
|
33
|
+
审批流程与接入说明见 [references/approval.md](references/approval.md)。
|
|
34
|
+
测试清单见 [references/testing.md](references/testing.md)。
|
|
35
|
+
详细能力清单见 [references/CAPABILITIES.md](references/CAPABILITIES.md)。
|
|
36
|
+
代码模板见 [references/TEMPLATES.md](references/TEMPLATES.md)。
|
|
34
37
|
|
|
35
38
|
## 模块结构约定
|
|
36
39
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Keystone 审批接入流程
|
|
2
|
+
|
|
3
|
+
## 目标
|
|
4
|
+
本文只描述审批接入所需的表单 schema 与 context 映射等开发工作,
|
|
5
|
+
流程节点与审批路径由人工在审批配置页面完成。
|
|
6
|
+
|
|
7
|
+
## 适用场景
|
|
8
|
+
- 新业务需要审批门禁(新业务类型)。
|
|
9
|
+
- 既有业务接入审批门禁(在业务表单提交或关键业务动作前加审批)。
|
|
10
|
+
|
|
11
|
+
## 不在范围
|
|
12
|
+
- 配置审批流程与节点(由人工在流程配置页面完成)。
|
|
13
|
+
- 修改 approval 基础模块或审批页面本身。
|
|
14
|
+
- 改造审批权限体系或审批路由结构。
|
|
15
|
+
|
|
16
|
+
## 开发路径选择
|
|
17
|
+
- Greenfield:新业务从零引入审批。
|
|
18
|
+
- Retrofit:已有业务接入审批。
|
|
19
|
+
|
|
20
|
+
## 开发步骤 A:新业务(Greenfield)
|
|
21
|
+
1) 前端配置 `approval.businessTypes` 标签映射(用于流程配置时选择表单),
|
|
22
|
+
流程配置页面的业务类型选择会作为 schema code 加载表单字段,需保持一致。
|
|
23
|
+
2) 注册表单 schema(条件节点字段来源)。
|
|
24
|
+
3) 在业务提交处创建审批实例,写入 context JSON 用于条件路由。
|
|
25
|
+
4) 绑定 OnApproved/OnRejected 回调更新业务状态。
|
|
26
|
+
5) 按测试清单执行(见 testing.md)。
|
|
27
|
+
|
|
28
|
+
## 开发步骤 B:已有业务接入(Retrofit)
|
|
29
|
+
1) 确认触发点与审批边界(业务表单提交或关键业务动作前)。
|
|
30
|
+
2) 将现有字段映射为 context JSON(供条件节点判断)。
|
|
31
|
+
3) 做幂等:按业务类型 + 业务 ID 查已有实例再创建。
|
|
32
|
+
4) pending 期间阻断业务动作(状态拦截或硬拦截)。
|
|
33
|
+
5) 决定是否回填存量数据(跳过或补建实例)。
|
|
34
|
+
|
|
35
|
+
## 业务侧改动清单
|
|
36
|
+
|
|
37
|
+
后端(业务模块内)
|
|
38
|
+
- 在业务 service/handler 中调用审批服务创建实例(必要时通过 FlowMatcher 选 flow)。
|
|
39
|
+
- 在业务模块注册审批回调,审批通过/驳回时更新业务状态。
|
|
40
|
+
- 需要数据范围过滤时,实现 ApprovalContextResolver 并在应用装配时注入。
|
|
41
|
+
- 如需默认 flow 或 schema,放在业务模块的迁移/seed 中,不改动审批模块。
|
|
42
|
+
|
|
43
|
+
前端(业务模块内)
|
|
44
|
+
- 配置 `approval.businessTypes` 标签映射,保证业务类型显示正确。
|
|
45
|
+
- 流程配置使用已有审批管理页面,不做二次开发。
|
|
46
|
+
|
|
47
|
+
## 权限说明
|
|
48
|
+
使用系统既有审批权限码:
|
|
49
|
+
approval:flow:view|create|edit|delete
|
|
50
|
+
approval:instance:view|approve|cancel
|
|
51
|
+
approval:record:view|export
|
|
52
|
+
|
|
53
|
+
## 集成自检
|
|
54
|
+
- Flow code <= 50 且使用大写;business type 用短字符串。
|
|
55
|
+
- approve/reject/cancel 使用 version 做乐观锁。
|
|
56
|
+
- 变更接口时更新 OpenAPI 合同。
|
|
57
|
+
- 新增默认流程时补迁移或 seed。
|
|
58
|
+
|
|
59
|
+
测试清单见 [testing.md](testing.md)。
|
|
60
|
+
|
|
61
|
+
## 参考入口
|
|
62
|
+
|
|
63
|
+
后端
|
|
64
|
+
- domain/approval/service/service.go
|
|
65
|
+
- domain/approval/service/flow_matcher.go
|
|
66
|
+
- domain/approval/service/callback_registry.go
|
|
67
|
+
- api/routes/approval_routes.go
|
|
68
|
+
- contracts/011-approvals/approvals-api.yaml
|
|
69
|
+
|
|
70
|
+
前端
|
|
71
|
+
- packages/keystone-web-core/src/platform/config/appConfig.ts
|
|
72
|
+
- packages/keystone-web-core/src/platform/core/approval/services/approvalService.ts
|
|
73
|
+
- packages/keystone-web-core/src/platform/core/approval/stores/approvalStore.ts
|
|
74
|
+
- packages/keystone-web-core/src/modules/keystone/approval/pages/*
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Keystone 开发测试清单
|
|
2
|
+
|
|
3
|
+
## 快速入口
|
|
4
|
+
- 仓库根目录:`pnpm test`(模板测试 + `go test ./...`)
|
|
5
|
+
- 仅 Go:`go test ./...`
|
|
6
|
+
- 模板项目:`pnpm -C packages/create-keystone-app/template test`
|
|
7
|
+
|
|
8
|
+
## 按改动范围选择
|
|
9
|
+
后端(api/domain/infra/bootstrap/server)
|
|
10
|
+
- `go test ./...`
|
|
11
|
+
- 若改动较大可补 `go vet ./...`
|
|
12
|
+
|
|
13
|
+
模板前端(packages/create-keystone-app/template/apps/web)
|
|
14
|
+
- `pnpm -C packages/create-keystone-app/template/apps/web typecheck`
|
|
15
|
+
- `pnpm -C packages/create-keystone-app/template/apps/web lint`
|
|
16
|
+
- `pnpm -C packages/create-keystone-app/template/apps/web test`
|
|
17
|
+
|
|
18
|
+
前端核心包(packages/keystone-web-core)
|
|
19
|
+
- `pnpm -C packages/keystone-web-core build`
|
|
20
|
+
|
|
21
|
+
脚手架/模板变更
|
|
22
|
+
- `pnpm scaffold:verify`
|
|
23
|
+
|
|
24
|
+
合同变更
|
|
25
|
+
- `pnpm contracts:check`
|
|
26
|
+
- 如更新了合同内容,执行 `pnpm contracts:sync`
|
|
27
|
+
|
|
28
|
+
## 写测试(必做)
|
|
29
|
+
后端
|
|
30
|
+
- 领域/服务逻辑新增或改动:在 `tests/unit/platform/service` 或对应 domain 下补单测。
|
|
31
|
+
- Repository 变更:在 `tests/unit/platform/repository` 补单测。
|
|
32
|
+
- 新增/调整 API:在 `tests/integration/platform/api` 补集成测试。
|
|
33
|
+
- 参考用例:`tests/unit/platform/approval/service_test.go`、`tests/integration/platform/api/approval_schema_test.go`。
|
|
34
|
+
|
|
35
|
+
前端(模板 web)
|
|
36
|
+
- 新增页面/交互:在 `packages/create-keystone-app/template/apps/web/tests` 新增 `*.test.tsx`。
|
|
37
|
+
- 使用 vitest + Testing Library(已在 `vite.config.ts` 配好 test 配置)。
|
|
38
|
+
|
|
39
|
+
前端核心包(keystone-web-core)
|
|
40
|
+
- 新增核心组件/逻辑:优先在模板 web 侧写集成测试覆盖。
|
|
41
|
+
- 如必须在包内写单测,需要补齐包内测试配置后再添加用例。
|
|
42
|
+
|
|
43
|
+
## 最小通过标准
|
|
44
|
+
- 单点改动跑对应子项;跨域改动直接跑 `pnpm test`。
|
|
@@ -144,6 +144,7 @@ github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM
|
|
|
144
144
|
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
|
145
145
|
github.com/robsuncn/keystone v0.1.1 h1:0BK2lL9wGjp9/0ZWnwoNnEHqGvAkGYLQbkwlVemDSzQ=
|
|
146
146
|
github.com/robsuncn/keystone v0.1.1/go.mod h1:VPNHWG9pZi00SRC8hqy47EvfxnI795/ZC1vSkLm6x1c=
|
|
147
|
+
github.com/robsuncn/keystone v0.2.0/go.mod h1:qIpuWlWXmuwy+lEuyMDLy5FLzjRWki/oJ3nGO5jyIFc=
|
|
147
148
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
|
148
149
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
149
150
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
|
@@ -6,7 +6,9 @@ import (
|
|
|
6
6
|
"github.com/gin-gonic/gin"
|
|
7
7
|
hcommon "github.com/robsuncn/keystone/api/handler/common"
|
|
8
8
|
"github.com/robsuncn/keystone/api/response"
|
|
9
|
+
"github.com/robsuncn/keystone/infra/i18n"
|
|
9
10
|
|
|
11
|
+
examplei18n "__APP_NAME__/apps/server/internal/modules/example/i18n"
|
|
10
12
|
"__APP_NAME__/apps/server/internal/modules/example/domain/models"
|
|
11
13
|
"__APP_NAME__/apps/server/internal/modules/example/domain/service"
|
|
12
14
|
)
|
|
@@ -38,14 +40,14 @@ const defaultTenantID uint = 1
|
|
|
38
40
|
|
|
39
41
|
func (h *ItemHandler) List(c *gin.Context) {
|
|
40
42
|
if h == nil || h.items == nil {
|
|
41
|
-
response.
|
|
43
|
+
response.ServiceUnavailableI18n(c, examplei18n.MsgServiceUnavailable)
|
|
42
44
|
return
|
|
43
45
|
}
|
|
44
46
|
tenantID := resolveTenantID(c)
|
|
45
47
|
|
|
46
48
|
items, err := h.items.List(c.Request.Context(), tenantID)
|
|
47
49
|
if err != nil {
|
|
48
|
-
response.
|
|
50
|
+
response.InternalErrorI18n(c, examplei18n.MsgItemLoadFailed)
|
|
49
51
|
return
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -54,14 +56,14 @@ func (h *ItemHandler) List(c *gin.Context) {
|
|
|
54
56
|
|
|
55
57
|
func (h *ItemHandler) Create(c *gin.Context) {
|
|
56
58
|
if h == nil || h.items == nil {
|
|
57
|
-
response.
|
|
59
|
+
response.ServiceUnavailableI18n(c, examplei18n.MsgServiceUnavailable)
|
|
58
60
|
return
|
|
59
61
|
}
|
|
60
62
|
tenantID := resolveTenantID(c)
|
|
61
63
|
|
|
62
64
|
var input itemInput
|
|
63
65
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
64
|
-
response.
|
|
66
|
+
response.BadRequestI18n(c, examplei18n.MsgInvalidPayload)
|
|
65
67
|
return
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -71,36 +73,34 @@ func (h *ItemHandler) Create(c *gin.Context) {
|
|
|
71
73
|
Status: input.Status,
|
|
72
74
|
})
|
|
73
75
|
if err != nil {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
response.
|
|
77
|
-
|
|
78
|
-
response.BadRequest(c, "invalid status")
|
|
79
|
-
default:
|
|
80
|
-
response.InternalError(c, "failed to create example item")
|
|
76
|
+
var i18nErr *i18n.I18nError
|
|
77
|
+
if errors.As(err, &i18nErr) {
|
|
78
|
+
response.BadRequestI18n(c, i18nErr.Key)
|
|
79
|
+
return
|
|
81
80
|
}
|
|
81
|
+
response.InternalErrorI18n(c, examplei18n.MsgItemCreateFailed)
|
|
82
82
|
return
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
response.
|
|
85
|
+
response.CreatedI18n(c, examplei18n.MsgItemCreated, item)
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
func (h *ItemHandler) Update(c *gin.Context) {
|
|
89
89
|
if h == nil || h.items == nil {
|
|
90
|
-
response.
|
|
90
|
+
response.ServiceUnavailableI18n(c, examplei18n.MsgServiceUnavailable)
|
|
91
91
|
return
|
|
92
92
|
}
|
|
93
93
|
tenantID := resolveTenantID(c)
|
|
94
94
|
|
|
95
95
|
id, err := hcommon.ParseUintParam(c, "id")
|
|
96
96
|
if err != nil || id == 0 {
|
|
97
|
-
response.
|
|
97
|
+
response.BadRequestI18n(c, examplei18n.MsgInvalidID)
|
|
98
98
|
return
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
var input itemUpdateInput
|
|
102
102
|
if err := c.ShouldBindJSON(&input); err != nil {
|
|
103
|
-
response.
|
|
103
|
+
response.BadRequestI18n(c, examplei18n.MsgInvalidPayload)
|
|
104
104
|
return
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -110,45 +110,48 @@ func (h *ItemHandler) Update(c *gin.Context) {
|
|
|
110
110
|
Status: input.Status,
|
|
111
111
|
})
|
|
112
112
|
if err != nil {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
response.InternalError(c, "failed to update example item")
|
|
113
|
+
var i18nErr *i18n.I18nError
|
|
114
|
+
if errors.As(err, &i18nErr) {
|
|
115
|
+
if i18nErr.Key == examplei18n.MsgItemNotFound {
|
|
116
|
+
response.NotFoundI18n(c, i18nErr.Key)
|
|
117
|
+
} else {
|
|
118
|
+
response.BadRequestI18n(c, i18nErr.Key)
|
|
119
|
+
}
|
|
120
|
+
return
|
|
122
121
|
}
|
|
122
|
+
response.InternalErrorI18n(c, examplei18n.MsgItemUpdateFailed)
|
|
123
123
|
return
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
response.
|
|
126
|
+
response.SuccessI18n(c, examplei18n.MsgItemUpdated, item)
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
func (h *ItemHandler) Delete(c *gin.Context) {
|
|
130
130
|
if h == nil || h.items == nil {
|
|
131
|
-
response.
|
|
131
|
+
response.ServiceUnavailableI18n(c, examplei18n.MsgServiceUnavailable)
|
|
132
132
|
return
|
|
133
133
|
}
|
|
134
134
|
tenantID := resolveTenantID(c)
|
|
135
135
|
|
|
136
136
|
id, err := hcommon.ParseUintParam(c, "id")
|
|
137
137
|
if err != nil || id == 0 {
|
|
138
|
-
response.
|
|
138
|
+
response.BadRequestI18n(c, examplei18n.MsgInvalidID)
|
|
139
139
|
return
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
if err := h.items.Delete(c.Request.Context(), tenantID, id); err != nil {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
var i18nErr *i18n.I18nError
|
|
144
|
+
if errors.As(err, &i18nErr) {
|
|
145
|
+
if i18nErr.Key == examplei18n.MsgItemNotFound {
|
|
146
|
+
response.NotFoundI18n(c, i18nErr.Key)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
146
149
|
}
|
|
147
|
-
response.
|
|
150
|
+
response.InternalErrorI18n(c, examplei18n.MsgItemDeleteFailed)
|
|
148
151
|
return
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
response.
|
|
154
|
+
response.SuccessI18n(c, examplei18n.MsgItemDeleted, gin.H{"id": id})
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
func resolveTenantID(c *gin.Context) uint {
|
|
@@ -7,3 +7,16 @@ var (
|
|
|
7
7
|
ErrTitleRequired = errors.New("title is required")
|
|
8
8
|
ErrStatusInvalid = errors.New("status is invalid")
|
|
9
9
|
)
|
|
10
|
+
|
|
11
|
+
// I18n version (uncomment when using keystone with i18n support):
|
|
12
|
+
//
|
|
13
|
+
// import (
|
|
14
|
+
// "github.com/robsuncn/keystone/infra/i18n"
|
|
15
|
+
// examplei18n "your-app/apps/server/internal/modules/example/i18n"
|
|
16
|
+
// )
|
|
17
|
+
//
|
|
18
|
+
// var (
|
|
19
|
+
// ErrItemNotFound = &i18n.I18nError{Key: examplei18n.MsgItemNotFound}
|
|
20
|
+
// ErrTitleRequired = &i18n.I18nError{Key: examplei18n.MsgTitleRequired}
|
|
21
|
+
// ErrStatusInvalid = &i18n.I18nError{Key: examplei18n.MsgStatusInvalid}
|
|
22
|
+
// )
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package examplei18n
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"embed"
|
|
5
|
+
|
|
6
|
+
"github.com/robsuncn/keystone/infra/i18n"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
//go:embed locales/*.json
|
|
10
|
+
var localeFS embed.FS
|
|
11
|
+
|
|
12
|
+
// RegisterLocales registers the example module's locale files with the i18n system.
|
|
13
|
+
// Call this during module initialization.
|
|
14
|
+
func RegisterLocales() error {
|
|
15
|
+
return i18n.LoadModuleLocales(localeFS, "locales")
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package examplei18n
|
|
2
|
+
|
|
3
|
+
// Example module i18n message keys
|
|
4
|
+
const (
|
|
5
|
+
// Item messages
|
|
6
|
+
MsgItemCreated = "example.item.created"
|
|
7
|
+
MsgItemUpdated = "example.item.updated"
|
|
8
|
+
MsgItemDeleted = "example.item.deleted"
|
|
9
|
+
MsgItemNotFound = "example.item.notFound"
|
|
10
|
+
MsgItemLoadFailed = "example.item.loadFailed"
|
|
11
|
+
MsgItemCreateFailed = "example.item.createFailed"
|
|
12
|
+
MsgItemUpdateFailed = "example.item.updateFailed"
|
|
13
|
+
MsgItemDeleteFailed = "example.item.deleteFailed"
|
|
14
|
+
|
|
15
|
+
// Validation messages
|
|
16
|
+
MsgTitleRequired = "example.validation.titleRequired"
|
|
17
|
+
MsgStatusInvalid = "example.validation.statusInvalid"
|
|
18
|
+
MsgInvalidID = "example.validation.invalidId"
|
|
19
|
+
MsgInvalidPayload = "example.validation.invalidPayload"
|
|
20
|
+
|
|
21
|
+
// Service messages
|
|
22
|
+
MsgServiceUnavailable = "example.service.unavailable"
|
|
23
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"example.item.created": "Item created successfully",
|
|
3
|
+
"example.item.updated": "Item updated successfully",
|
|
4
|
+
"example.item.deleted": "Item deleted successfully",
|
|
5
|
+
"example.item.notFound": "Item not found",
|
|
6
|
+
"example.item.loadFailed": "Failed to load items",
|
|
7
|
+
"example.item.createFailed": "Failed to create item",
|
|
8
|
+
"example.item.updateFailed": "Failed to update item",
|
|
9
|
+
"example.item.deleteFailed": "Failed to delete item",
|
|
10
|
+
"example.validation.titleRequired": "Title is required",
|
|
11
|
+
"example.validation.statusInvalid": "Invalid status value",
|
|
12
|
+
"example.validation.invalidId": "Invalid ID",
|
|
13
|
+
"example.validation.invalidPayload": "Invalid request payload",
|
|
14
|
+
"example.service.unavailable": "Example service is unavailable",
|
|
15
|
+
"permission.example.item": "Example Items",
|
|
16
|
+
"permission.example.item.view": "View Items",
|
|
17
|
+
"permission.example.item.manage": "Manage Items"
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"example.item.created": "项目创建成功",
|
|
3
|
+
"example.item.updated": "项目更新成功",
|
|
4
|
+
"example.item.deleted": "项目删除成功",
|
|
5
|
+
"example.item.notFound": "项目不存在",
|
|
6
|
+
"example.item.loadFailed": "加载项目列表失败",
|
|
7
|
+
"example.item.createFailed": "创建项目失败",
|
|
8
|
+
"example.item.updateFailed": "更新项目失败",
|
|
9
|
+
"example.item.deleteFailed": "删除项目失败",
|
|
10
|
+
"example.validation.titleRequired": "标题不能为空",
|
|
11
|
+
"example.validation.statusInvalid": "状态值无效",
|
|
12
|
+
"example.validation.invalidId": "无效的 ID",
|
|
13
|
+
"example.validation.invalidPayload": "请求参数无效",
|
|
14
|
+
"example.service.unavailable": "示例服务暂不可用",
|
|
15
|
+
"permission.example.item": "示例项目",
|
|
16
|
+
"permission.example.item.view": "查看项目",
|
|
17
|
+
"permission.example.item.manage": "管理项目"
|
|
18
|
+
}
|