@robsun/create-keystone-app 0.2.13 → 0.4.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.
- package/README.md +46 -43
- package/dist/create-keystone-app.js +347 -10
- package/dist/create-module.js +1219 -607
- package/package.json +22 -23
- package/template/.claude/skills/keystone-implement/SKILL.md +113 -0
- package/template/.claude/skills/keystone-implement/references/CHECKLIST.md +91 -0
- package/template/.claude/skills/keystone-implement/references/PATTERNS.md +1088 -0
- package/template/.claude/skills/keystone-implement/references/SCHEMA.md +135 -0
- package/template/.claude/skills/keystone-implement/references/TESTING.md +231 -0
- package/template/.claude/skills/keystone-requirements/SKILL.md +296 -0
- package/template/.claude/skills/keystone-requirements/references/CONFIRM_TEMPLATE.md +170 -0
- package/template/.claude/skills/keystone-requirements/references/SCHEMA.md +135 -0
- package/template/.eslintrc.js +3 -0
- package/template/.github/workflows/ci.yml +30 -0
- package/template/.github/workflows/release.yml +32 -0
- package/template/.golangci.yml +11 -0
- package/template/README.md +81 -73
- package/template/apps/server/README.md +8 -0
- package/template/apps/server/cmd/server/main.go +27 -185
- package/template/apps/server/config.example.yaml +31 -1
- package/template/apps/server/config.yaml +31 -1
- package/template/apps/server/go.mod +60 -18
- package/template/apps/server/go.sum +183 -31
- package/template/apps/server/internal/frontend/embed.go +3 -8
- package/template/apps/server/internal/modules/example/README.md +18 -0
- package/template/apps/server/internal/modules/example/api/handler/handler_test.go +9 -0
- package/template/apps/server/internal/modules/example/api/handler/item_handler.go +468 -165
- package/template/apps/server/internal/modules/example/bootstrap/seeds/item.go +217 -8
- package/template/apps/server/internal/modules/example/domain/models/item.go +40 -7
- package/template/apps/server/internal/modules/example/domain/service/approval_callback.go +68 -0
- package/template/apps/server/internal/modules/example/domain/service/approval_schema.go +41 -0
- package/template/apps/server/internal/modules/example/domain/service/errors.go +20 -22
- package/template/apps/server/internal/modules/example/domain/service/item_service.go +267 -7
- package/template/apps/server/internal/modules/example/domain/service/item_service_test.go +281 -0
- package/template/apps/server/internal/modules/example/i18n/keys.go +32 -20
- package/template/apps/server/internal/modules/example/i18n/locales/en-US.json +30 -18
- package/template/apps/server/internal/modules/example/i18n/locales/zh-CN.json +30 -18
- package/template/apps/server/internal/modules/example/infra/exporter/item_exporter.go +119 -0
- package/template/apps/server/internal/modules/example/infra/importer/item_importer.go +77 -0
- package/template/apps/server/internal/modules/example/infra/repository/item_repository.go +99 -49
- package/template/apps/server/internal/modules/example/module.go +171 -97
- package/template/apps/server/internal/modules/example/tests/integration_test.go +7 -0
- package/template/apps/server/internal/modules/manifest.go +7 -7
- package/template/apps/web/README.md +4 -2
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/app.config.ts +8 -6
- package/template/apps/web/src/index.css +7 -3
- package/template/apps/web/src/main.tsx +2 -5
- package/template/apps/web/src/modules/example/help/en-US/faq.md +27 -0
- package/template/apps/web/src/modules/example/help/en-US/items.md +30 -0
- package/template/apps/web/src/modules/example/help/en-US/overview.md +31 -0
- package/template/apps/web/src/modules/example/help/zh-CN/faq.md +27 -0
- package/template/apps/web/src/modules/example/help/zh-CN/items.md +31 -0
- package/template/apps/web/src/modules/example/help/zh-CN/overview.md +32 -0
- package/template/apps/web/src/modules/example/locales/en-US/example.json +99 -32
- package/template/apps/web/src/modules/example/locales/zh-CN/example.json +85 -18
- package/template/apps/web/src/modules/example/pages/ExampleItemsPage.tsx +840 -237
- package/template/apps/web/src/modules/example/services/exampleItems.ts +79 -8
- package/template/apps/web/src/modules/example/types.ts +14 -1
- package/template/apps/web/src/modules/index.ts +1 -0
- package/template/apps/web/vite.config.ts +9 -3
- package/template/docs/CONVENTIONS.md +10 -7
- package/template/package.json +4 -5
- package/template/pnpm-lock.yaml +76 -5
- package/template/scripts/build.bat +15 -3
- package/template/scripts/build.sh +9 -3
- package/template/scripts/check-help.js +249 -0
- package/template/scripts/compress-assets.js +89 -0
- package/template/scripts/test.bat +23 -0
- package/template/scripts/test.sh +16 -0
- package/template/.claude/skills/keystone-dev/SKILL.md +0 -103
- package/template/.claude/skills/keystone-dev/references/APPROVAL.md +0 -121
- package/template/.claude/skills/keystone-dev/references/CAPABILITIES.md +0 -261
- package/template/.claude/skills/keystone-dev/references/TEMPLATES.md +0 -532
- package/template/.claude/skills/keystone-dev/references/TESTING.md +0 -44
- package/template/.codex/skills/keystone-dev/SKILL.md +0 -103
- package/template/.codex/skills/keystone-dev/references/APPROVAL.md +0 -121
- package/template/.codex/skills/keystone-dev/references/CAPABILITIES.md +0 -261
- package/template/.codex/skills/keystone-dev/references/TEMPLATES.md +0 -532
- package/template/.codex/skills/keystone-dev/references/TESTING.md +0 -44
- package/template/apps/server/internal/app/routes/module_routes.go +0 -16
- package/template/apps/server/internal/app/routes/routes.go +0 -226
- package/template/apps/server/internal/app/startup/startup.go +0 -74
- package/template/apps/server/internal/frontend/handler.go +0 -122
- package/template/apps/server/internal/modules/registry.go +0 -145
- package/template/apps/web/src/modules/example/help/faq.md +0 -23
- package/template/apps/web/src/modules/example/help/items.md +0 -26
- package/template/apps/web/src/modules/example/help/overview.md +0 -25
|
@@ -1,18 +1,30 @@
|
|
|
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.
|
|
6
|
-
"example.item.
|
|
7
|
-
"example.item.
|
|
8
|
-
"example.item.
|
|
9
|
-
"example.item.
|
|
10
|
-
"example.
|
|
11
|
-
"example.
|
|
12
|
-
"example.
|
|
13
|
-
"example.
|
|
14
|
-
"example.
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
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.submitted": "Item submitted for approval",
|
|
6
|
+
"example.item.cancelled": "Approval cancelled",
|
|
7
|
+
"example.item.notFound": "Item not found",
|
|
8
|
+
"example.item.loadFailed": "Failed to load items",
|
|
9
|
+
"example.item.createFailed": "Failed to create item",
|
|
10
|
+
"example.item.updateFailed": "Failed to update item",
|
|
11
|
+
"example.item.deleteFailed": "Failed to delete item",
|
|
12
|
+
"example.item.submitFailed": "Failed to submit item",
|
|
13
|
+
"example.item.cancelFailed": "Failed to cancel approval",
|
|
14
|
+
"example.item.exportFailed": "Failed to export items",
|
|
15
|
+
"example.validation.titleRequired": "Title is required",
|
|
16
|
+
"example.validation.statusInvalid": "Invalid status value",
|
|
17
|
+
"example.validation.categoryInvalid": "Invalid category",
|
|
18
|
+
"example.validation.amountInvalid": "Amount must be zero or greater",
|
|
19
|
+
"example.validation.itemLocked": "Item cannot be edited or deleted in the current status",
|
|
20
|
+
"example.validation.submitterRequired": "Submitter is required",
|
|
21
|
+
"example.validation.approvalNotReady": "Approval flow is not ready",
|
|
22
|
+
"example.validation.approvalNotPending": "Only pending approvals can be cancelled",
|
|
23
|
+
"example.validation.approvalInstanceMissing": "Approval instance is missing",
|
|
24
|
+
"example.validation.invalidId": "Invalid ID",
|
|
25
|
+
"example.validation.invalidPayload": "Invalid request payload",
|
|
26
|
+
"example.service.unavailable": "Example service is unavailable",
|
|
27
|
+
"permission.example.item": "Example Items",
|
|
28
|
+
"permission.example.item.view": "View Items",
|
|
29
|
+
"permission.example.item.manage": "Manage Items"
|
|
30
|
+
}
|
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
{
|
|
2
|
-
"example.item.created": "
|
|
3
|
-
"example.item.updated": "
|
|
4
|
-
"example.item.deleted": "
|
|
5
|
-
"example.item.
|
|
6
|
-
"example.item.
|
|
7
|
-
"example.item.
|
|
8
|
-
"example.item.
|
|
9
|
-
"example.item.
|
|
10
|
-
"example.
|
|
11
|
-
"example.
|
|
12
|
-
"example.
|
|
13
|
-
"example.
|
|
14
|
-
"example.
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
1
|
+
{
|
|
2
|
+
"example.item.created": "事项创建成功",
|
|
3
|
+
"example.item.updated": "事项更新成功",
|
|
4
|
+
"example.item.deleted": "事项删除成功",
|
|
5
|
+
"example.item.submitted": "事项已提交审批",
|
|
6
|
+
"example.item.cancelled": "事项已取消审批",
|
|
7
|
+
"example.item.notFound": "事项不存在",
|
|
8
|
+
"example.item.loadFailed": "加载事项列表失败",
|
|
9
|
+
"example.item.createFailed": "创建事项失败",
|
|
10
|
+
"example.item.updateFailed": "更新事项失败",
|
|
11
|
+
"example.item.deleteFailed": "删除事项失败",
|
|
12
|
+
"example.item.submitFailed": "提交审批失败",
|
|
13
|
+
"example.item.cancelFailed": "取消审批失败",
|
|
14
|
+
"example.item.exportFailed": "导出事项失败",
|
|
15
|
+
"example.validation.titleRequired": "标题不能为空",
|
|
16
|
+
"example.validation.statusInvalid": "状态无效",
|
|
17
|
+
"example.validation.categoryInvalid": "类别无效",
|
|
18
|
+
"example.validation.amountInvalid": "金额不能小于 0",
|
|
19
|
+
"example.validation.itemLocked": "当前状态不允许编辑或删除",
|
|
20
|
+
"example.validation.submitterRequired": "缺少提交人信息",
|
|
21
|
+
"example.validation.approvalNotReady": "审批流程未就绪",
|
|
22
|
+
"example.validation.approvalNotPending": "仅可取消审批中的事项",
|
|
23
|
+
"example.validation.approvalInstanceMissing": "审批实例不存在",
|
|
24
|
+
"example.validation.invalidId": "无效的 ID",
|
|
25
|
+
"example.validation.invalidPayload": "请求参数无效",
|
|
26
|
+
"example.service.unavailable": "示例服务暂不可用",
|
|
27
|
+
"permission.example.item": "示例事项",
|
|
28
|
+
"permission.example.item.view": "查看事项",
|
|
29
|
+
"permission.example.item.manage": "管理事项"
|
|
30
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
package exporter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"time"
|
|
7
|
+
|
|
8
|
+
approvalmodels "github.com/robsuncn/keystone/domain/approval/models"
|
|
9
|
+
coreservice "github.com/robsuncn/keystone/domain/service"
|
|
10
|
+
coreexporter "github.com/robsuncn/keystone/infra/exporter"
|
|
11
|
+
"github.com/robsuncn/keystone/infra/jobs"
|
|
12
|
+
|
|
13
|
+
examplemodels "__APP_NAME__/apps/server/internal/modules/example/domain/models"
|
|
14
|
+
exampleservice "__APP_NAME__/apps/server/internal/modules/example/domain/service"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const ExportTypeExampleItems = "example_items"
|
|
18
|
+
|
|
19
|
+
func RegisterItemExporter(reg *coreexporter.Service, items *exampleservice.ItemService) error {
|
|
20
|
+
if reg == nil || items == nil {
|
|
21
|
+
return nil
|
|
22
|
+
}
|
|
23
|
+
return reg.Register(ExportTypeExampleItems, coreexporter.Definition{
|
|
24
|
+
Format: coreexporter.FormatExcel,
|
|
25
|
+
Build: func(ctx context.Context, payload jobs.ExportJobPayload, tracker coreexporter.ProgressTracker) (coreexporter.Dataset, error) {
|
|
26
|
+
tenantID, ok := coreservice.TenantIDFromContext(ctx)
|
|
27
|
+
if !ok || tenantID == 0 {
|
|
28
|
+
return coreexporter.Dataset{}, fmt.Errorf("tenant id missing")
|
|
29
|
+
}
|
|
30
|
+
filter := buildFilter(payload.Options)
|
|
31
|
+
filter.Page = 1
|
|
32
|
+
filter.PageSize = 0
|
|
33
|
+
|
|
34
|
+
list, _, err := items.List(ctx, tenantID, filter)
|
|
35
|
+
if err != nil {
|
|
36
|
+
return coreexporter.Dataset{}, err
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
headers := []string{"Number", "Title", "Category", "Amount", "Status", "Approval Status", "Created At", "Updated At"}
|
|
40
|
+
rows := make([][]string, 0, len(list))
|
|
41
|
+
for idx, item := range list {
|
|
42
|
+
rows = append(rows, []string{
|
|
43
|
+
item.Number,
|
|
44
|
+
item.Title,
|
|
45
|
+
string(item.Category),
|
|
46
|
+
fmt.Sprintf("%.2f", item.Amount),
|
|
47
|
+
string(item.Status),
|
|
48
|
+
string(item.ApprovalStatus),
|
|
49
|
+
formatTime(item.CreatedAt),
|
|
50
|
+
formatTime(item.UpdatedAt),
|
|
51
|
+
})
|
|
52
|
+
if tracker != nil {
|
|
53
|
+
_ = tracker.Report(ctx, coreexporter.Progress{Total: len(list), Processed: idx + 1})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return coreexporter.Dataset{Headers: headers, Rows: rows}, nil
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func buildFilter(options map[string]interface{}) exampleservice.ItemListFilter {
|
|
63
|
+
filter := exampleservice.ItemListFilter{}
|
|
64
|
+
if options == nil {
|
|
65
|
+
return filter
|
|
66
|
+
}
|
|
67
|
+
if raw, ok := options["keyword"].(string); ok {
|
|
68
|
+
filter.Keyword = raw
|
|
69
|
+
}
|
|
70
|
+
if raw, ok := options["status"].(string); ok && raw != "" {
|
|
71
|
+
status := examplemodels.ItemStatus(raw)
|
|
72
|
+
if status.IsValid() {
|
|
73
|
+
filter.Status = &status
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if raw, ok := options["approval_status"].(string); ok && raw != "" {
|
|
77
|
+
status := approvalmodels.InstanceStatus(raw)
|
|
78
|
+
filter.ApprovalStatus = &status
|
|
79
|
+
}
|
|
80
|
+
if raw, ok := options["category"].(string); ok && raw != "" {
|
|
81
|
+
category := examplemodels.ItemCategory(raw)
|
|
82
|
+
if category.IsValid() {
|
|
83
|
+
filter.Category = &category
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if raw, ok := options["start_date"].(string); ok && raw != "" {
|
|
87
|
+
if parsed, err := parseDate(raw, false); err == nil {
|
|
88
|
+
filter.StartDate = parsed
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if raw, ok := options["end_date"].(string); ok && raw != "" {
|
|
92
|
+
if parsed, err := parseDate(raw, true); err == nil {
|
|
93
|
+
filter.EndDate = parsed
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return filter
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func parseDate(value string, endOfDay bool) (*time.Time, error) {
|
|
100
|
+
layouts := []string{time.RFC3339, "2006-01-02"}
|
|
101
|
+
for _, layout := range layouts {
|
|
102
|
+
parsed, err := time.Parse(layout, value)
|
|
103
|
+
if err != nil {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if endOfDay && layout == "2006-01-02" {
|
|
107
|
+
parsed = parsed.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
|
108
|
+
}
|
|
109
|
+
return &parsed, nil
|
|
110
|
+
}
|
|
111
|
+
return nil, fmt.Errorf("invalid date")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func formatTime(value time.Time) string {
|
|
115
|
+
if value.IsZero() {
|
|
116
|
+
return ""
|
|
117
|
+
}
|
|
118
|
+
return value.UTC().Format(time.RFC3339)
|
|
119
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
package importer
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strconv"
|
|
7
|
+
"strings"
|
|
8
|
+
|
|
9
|
+
coreimporter "github.com/robsuncn/keystone/infra/importer"
|
|
10
|
+
coreservice "github.com/robsuncn/keystone/domain/service"
|
|
11
|
+
|
|
12
|
+
examplemodels "__APP_NAME__/apps/server/internal/modules/example/domain/models"
|
|
13
|
+
exampleservice "__APP_NAME__/apps/server/internal/modules/example/domain/service"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const ImportTypeExampleItems = "example_items"
|
|
17
|
+
|
|
18
|
+
func RegisterItemImporter(reg *coreimporter.Service, items *exampleservice.ItemService) error {
|
|
19
|
+
if reg == nil || items == nil {
|
|
20
|
+
return nil
|
|
21
|
+
}
|
|
22
|
+
return reg.Register(ImportTypeExampleItems, coreimporter.Definition{
|
|
23
|
+
Format: coreimporter.FormatExcel,
|
|
24
|
+
Validator: coreimporter.RequiredColumnsValidator{
|
|
25
|
+
Columns: []string{"title"},
|
|
26
|
+
},
|
|
27
|
+
Handler: func(ctx context.Context, record coreimporter.Record) error {
|
|
28
|
+
tenantID, ok := coreservice.TenantIDFromContext(ctx)
|
|
29
|
+
if !ok || tenantID == 0 {
|
|
30
|
+
return fmt.Errorf("tenant id missing")
|
|
31
|
+
}
|
|
32
|
+
input := exampleservice.ItemInput{
|
|
33
|
+
Title: record["title"],
|
|
34
|
+
Description: record["description"],
|
|
35
|
+
Category: parseCategory(record["category"]),
|
|
36
|
+
Amount: parseAmount(record["amount"]),
|
|
37
|
+
Status: parseStatus(record["status"]),
|
|
38
|
+
}
|
|
39
|
+
_, err := items.Create(ctx, tenantID, input)
|
|
40
|
+
return err
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func parseCategory(raw string) examplemodels.ItemCategory {
|
|
46
|
+
value := examplemodels.ItemCategory(strings.TrimSpace(raw))
|
|
47
|
+
if value == "" {
|
|
48
|
+
return examplemodels.CategoryGeneral
|
|
49
|
+
}
|
|
50
|
+
if value.IsValid() {
|
|
51
|
+
return value
|
|
52
|
+
}
|
|
53
|
+
return examplemodels.CategoryGeneral
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func parseStatus(raw string) examplemodels.ItemStatus {
|
|
57
|
+
value := examplemodels.ItemStatus(strings.TrimSpace(raw))
|
|
58
|
+
if value == "" {
|
|
59
|
+
return examplemodels.StatusDraft
|
|
60
|
+
}
|
|
61
|
+
if value.IsValid() {
|
|
62
|
+
return value
|
|
63
|
+
}
|
|
64
|
+
return examplemodels.StatusDraft
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func parseAmount(raw string) float64 {
|
|
68
|
+
trimmed := strings.TrimSpace(raw)
|
|
69
|
+
if trimmed == "" {
|
|
70
|
+
return 0
|
|
71
|
+
}
|
|
72
|
+
amount, err := strconv.ParseFloat(trimmed, 64)
|
|
73
|
+
if err != nil {
|
|
74
|
+
return 0
|
|
75
|
+
}
|
|
76
|
+
return amount
|
|
77
|
+
}
|
|
@@ -1,49 +1,99 @@
|
|
|
1
|
-
package repository
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"errors"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
package repository
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"gorm.io/gorm"
|
|
9
|
+
|
|
10
|
+
"github.com/robsuncn/keystone/domain/dbutil"
|
|
11
|
+
|
|
12
|
+
"__APP_NAME__/apps/server/internal/modules/example/domain/models"
|
|
13
|
+
"__APP_NAME__/apps/server/internal/modules/example/domain/service"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
type ItemRepository struct {
|
|
17
|
+
db *gorm.DB
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func NewItemRepository(db *gorm.DB) *ItemRepository {
|
|
21
|
+
return &ItemRepository{db: db}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func (r *ItemRepository) List(ctx context.Context, tenantID uint, filter service.ItemListFilter) ([]models.ExampleItem, int64, error) {
|
|
25
|
+
var items []models.ExampleItem
|
|
26
|
+
query := r.getDB(ctx).WithContext(ctx).
|
|
27
|
+
Model(&models.ExampleItem{}).
|
|
28
|
+
Where("tenant_id = ?", tenantID)
|
|
29
|
+
|
|
30
|
+
if filter.Keyword != "" {
|
|
31
|
+
keyword := strings.ToLower(strings.TrimSpace(filter.Keyword))
|
|
32
|
+
if keyword != "" {
|
|
33
|
+
like := "%" + keyword + "%"
|
|
34
|
+
query = query.Where("LOWER(title) LIKE ? OR LOWER(number) LIKE ?", like, like)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if filter.Status != nil && *filter.Status != "" {
|
|
38
|
+
query = query.Where("status = ?", *filter.Status)
|
|
39
|
+
}
|
|
40
|
+
if filter.ApprovalStatus != nil && *filter.ApprovalStatus != "" {
|
|
41
|
+
query = query.Where("approval_status = ?", *filter.ApprovalStatus)
|
|
42
|
+
}
|
|
43
|
+
if filter.Category != nil && *filter.Category != "" {
|
|
44
|
+
query = query.Where("category = ?", *filter.Category)
|
|
45
|
+
}
|
|
46
|
+
if filter.StartDate != nil {
|
|
47
|
+
query = query.Where("created_at >= ?", *filter.StartDate)
|
|
48
|
+
}
|
|
49
|
+
if filter.EndDate != nil {
|
|
50
|
+
query = query.Where("created_at <= ?", *filter.EndDate)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var total int64
|
|
54
|
+
if err := query.Count(&total).Error; err != nil {
|
|
55
|
+
return nil, 0, err
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if filter.PageSize > 0 {
|
|
59
|
+
page := filter.Page
|
|
60
|
+
if page < 1 {
|
|
61
|
+
page = 1
|
|
62
|
+
}
|
|
63
|
+
offset := (page - 1) * filter.PageSize
|
|
64
|
+
query = query.Limit(filter.PageSize).Offset(offset)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
err := query.Order("created_at desc").Find(&items).Error
|
|
68
|
+
return items, total, err
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func (r *ItemRepository) FindByID(tenantID, id uint) (*models.ExampleItem, error) {
|
|
72
|
+
var item models.ExampleItem
|
|
73
|
+
err := r.getDB(nil).Where("tenant_id = ? AND id = ?", tenantID, id).First(&item).Error
|
|
74
|
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
75
|
+
return nil, service.ErrItemNotFound
|
|
76
|
+
}
|
|
77
|
+
return &item, err
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func (r *ItemRepository) Create(ctx context.Context, item *models.ExampleItem) error {
|
|
81
|
+
return r.getDB(ctx).WithContext(ctx).Create(item).Error
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func (r *ItemRepository) Update(ctx context.Context, item *models.ExampleItem) error {
|
|
85
|
+
return r.getDB(ctx).WithContext(ctx).Save(item).Error
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func (r *ItemRepository) Delete(ctx context.Context, item *models.ExampleItem) error {
|
|
89
|
+
return r.getDB(ctx).WithContext(ctx).Delete(item).Error
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
func (r *ItemRepository) getDB(ctx context.Context) *gorm.DB {
|
|
93
|
+
if ctx != nil {
|
|
94
|
+
if tx := dbutil.TxFromContext(ctx); tx != nil {
|
|
95
|
+
return tx
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return r.db
|
|
99
|
+
}
|