优化结构

This commit is contained in:
Yvan 2026-08-22 12:13:16 +08:00
parent d987244d0e
commit b27a66e9d3
200 changed files with 1794 additions and 1715 deletions

View File

@ -10,6 +10,7 @@ import (
"kra/internal/app"
"kra/internal/biz"
systembiz "kra/internal/biz/system"
"kra/internal/conf"
"kra/internal/data"
"kra/internal/initialize"
@ -37,6 +38,7 @@ func wireApp(*conf.Server, *conf.Runtime, *slog.Logger, *logging.ReloadableLogge
worker.ProviderSet,
app.Catalog,
app.TaskRegistry,
runtimeContributions,
app.Runtime,
data.ProviderSet,
integration.ProviderSet,
@ -44,7 +46,7 @@ func wireApp(*conf.Server, *conf.Runtime, *slog.Logger, *logging.ReloadableLogge
wire.Bind(new(initialize.Backend), new(*data.Data)),
wire.Bind(new(cache.RedisProvider), new(*data.Data)),
wire.Bind(new(middleware.TokenAuthenticator), new(*service.AuthService)),
wire.Bind(new(biz.TaskMethodRegistry), new(*platformtask.Registry)),
wire.Bind(new(systembiz.TaskMethodRegistry), new(*platformtask.Registry)),
biz.ProviderSet,
service.ProviderSet,
newApp,

61
cmd/wire_gen.go generated
View File

@ -9,7 +9,7 @@ package main
import (
"github.com/go-kratos/kratos/v3"
"kra/internal/app"
"kra/internal/biz"
system2 "kra/internal/biz/system"
"kra/internal/conf"
"kra/internal/data"
"kra/internal/data/payment"
@ -49,97 +49,97 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
}
authorityAccessRepo := system.NewAuthorityAccessRepo(dataData)
apiRepo := system.NewAPIRepo(dataData)
accessControlUsecase := biz.NewAccessControlUsecase(authorityAccessRepo, apiRepo)
accessControlUsecase := system2.NewAccessControlUsecase(authorityAccessRepo, apiRepo)
accessControlService := service.NewAccessControlService(accessControlUsecase)
userRepo := system.NewUserRepo(dataData)
userUsecase := biz.NewUserUsecase(userRepo)
userUsecase := system2.NewUserUsecase(userRepo)
securityRepo := system.NewSecurityRepo(dataData)
bizCache := cache.New(dataData)
systemCache := cache.New(dataData)
runtimeSettings := system.NewRuntimeSettings(runtime)
apiTokenRepo := system.NewAPITokenRepo(dataData)
tokenUsecase := biz.NewTokenUsecase(apiTokenRepo)
securityUsecase := biz.NewSecurityUsecase(securityRepo, bizCache, runtimeSettings, tokenUsecase)
tokenUsecase := system2.NewTokenUsecase(apiTokenRepo)
securityUsecase := system2.NewSecurityUsecase(securityRepo, systemCache, runtimeSettings, tokenUsecase)
tokenIssuer := system.NewTokenIssuer(runtimeSettings)
auditRecordRepo := system.NewAuditRecorderRepo(dataData)
authenticationUsecase := biz.NewAuthenticationUsecase(userUsecase, securityUsecase, tokenIssuer, auditRecordRepo)
authenticationUsecase := system2.NewAuthenticationUsecase(userUsecase, securityUsecase, tokenIssuer, auditRecordRepo)
authService := service.NewAuthService(authenticationUsecase)
securityService := service.NewSecurityService(securityUsecase)
auditRecorderUsecase := biz.NewAuditRecorderUsecase(auditRecordRepo)
auditRecorderUsecase := system2.NewAuditRecorderUsecase(auditRecordRepo)
auditRecorder := service.NewAuditRecorder(auditRecorderUsecase)
authorityUsecase := biz.NewAuthorityUsecase(authorityAccessRepo)
authorityUsecase := system2.NewAuthorityUsecase(authorityAccessRepo)
authorityService := service.NewAuthorityService(authorityUsecase)
authority := handler.NewAuthority(authorityService)
menuRepo := system.NewMenuRepo(dataData)
menuUsecase := biz.NewMenuUsecase(menuRepo)
menuUsecase := system2.NewMenuUsecase(menuRepo)
menuService := service.NewMenuService(menuUsecase)
menu := handler.NewMenu(menuService)
apiUsecase := biz.NewAPIUsecase(apiRepo)
apiUsecase := system2.NewAPIUsecase(apiRepo)
apiService := service.NewAPIService(apiUsecase, runtimeSettings)
api := handler.NewAPI(apiService)
permissionRepo := system.NewPermissionRepo(dataData)
permissionUsecase := biz.NewPermissionUsecase(permissionRepo)
permissionUsecase := system2.NewPermissionUsecase(permissionRepo)
permissionService := service.NewPermissionService(permissionUsecase)
permission := handler.NewPermission(permissionService)
departmentRepo := system.NewDepartmentRepo(dataData)
departmentUsecase := biz.NewDepartmentUsecase(departmentRepo)
departmentUsecase := system2.NewDepartmentUsecase(departmentRepo)
departmentService := service.NewDepartmentService(departmentUsecase)
positionRepo := system.NewPositionRepo(dataData)
positionUsecase := biz.NewPositionUsecase(positionRepo)
positionUsecase := system2.NewPositionUsecase(positionRepo)
positionService := service.NewPositionService(positionUsecase)
organization := handler.NewOrganization(departmentService, positionService)
announcementRepo := system.NewAnnouncementRepo(dataData)
announcementUsecase := biz.NewAnnouncementUsecase(announcementRepo)
announcementUsecase := system2.NewAnnouncementUsecase(announcementRepo)
announcementService := service.NewAnnouncementService(announcementUsecase)
announcement := handler.NewAnnouncement(announcementService)
emailRepo := email.NewEmailRepo(runtime)
emailUsecase := biz.NewEmailUsecase(emailRepo)
emailUsecase := system2.NewEmailUsecase(emailRepo)
emailService := service.NewEmailService(emailUsecase)
handlerEmail := handler.NewEmail(emailService)
paymentRepo := payment.NewPaymentRepo(dataData)
paymentOrderRepo := payment.NewPaymentOrderRepo(dataData)
paymentUsecase := biz.NewPaymentUsecase(paymentRepo, paymentOrderRepo, logger)
paymentUsecase := system2.NewPaymentUsecase(paymentRepo, paymentOrderRepo, logger)
paymentService := service.NewPaymentService(paymentUsecase)
handlerPayment := handler.NewPayment(paymentService)
taskRepo := system.NewTaskRepo(dataData)
registry := app.TaskRegistry(catalog)
taskUsecase := biz.NewTaskUsecaseWithRegistry(taskRepo, registry)
taskUsecase := system2.NewTaskUsecaseWithRegistry(taskRepo, registry)
mediaRepo := system.NewMediaRepo(dataData)
mediaUsecase := biz.NewMediaUsecase(mediaRepo, reloadable, runtimeSettings)
mediaUsecase := system2.NewMediaUsecase(mediaRepo, reloadable, runtimeSettings)
taskExecutor := worker.NewTaskExecutorWithRegistry(taskUsecase, mediaUsecase, runtime, registry)
taskScheduler := worker.NewTaskScheduler(taskUsecase, authorityUsecase, taskExecutor, logger)
taskRuntime := worker.NewTaskRuntime(taskScheduler)
taskApplicationUsecase := biz.NewTaskApplicationUsecase(taskUsecase, taskRuntime)
taskApplicationUsecase := system2.NewTaskApplicationUsecase(taskUsecase, taskRuntime)
taskService := service.NewTaskService(taskApplicationUsecase)
task := handler.NewTask(taskService)
mediaService := service.NewMediaService(mediaUsecase, runtimeSettings)
media := handler.NewMedia(mediaService)
auditQueryRepo := system.NewAuditRepo(dataData)
auditUsecase := biz.NewAuditUsecase(auditQueryRepo)
auditUsecase := system2.NewAuditUsecase(auditQueryRepo)
auditService := service.NewAuditService(auditUsecase)
logFileRepo := system.NewLogFileRepo(dataData)
logViewerUsecase := biz.NewLogViewerUsecase(logFileRepo)
logViewerUsecase := system2.NewLogViewerUsecase(logFileRepo)
logViewerService := service.NewLogViewerService(logViewerUsecase)
audit := handler.NewAudit(auditService, auditRecorder, logViewerService, logger)
exportRepo := system.NewExportRepo(dataData)
exportUsecase := biz.NewExportUsecase(exportRepo)
exportService := service.NewExportService(exportUsecase, bizCache)
exportUsecase := system2.NewExportUsecase(exportRepo)
exportService := service.NewExportService(exportUsecase, systemCache)
export := handler.NewExport(exportService)
versionRepo := system.NewVersionRepo(dataData)
versionUsecase := biz.NewVersionUsecase(versionRepo)
versionUsecase := system2.NewVersionUsecase(versionRepo)
versionService := service.NewVersionService(versionUsecase)
version := handler.NewVersion(versionService)
dictionaryRepo := system.NewDictionaryRepo(dataData)
dictionaryUsecase := biz.NewDictionaryUsecase(dictionaryRepo)
dictionaryUsecase := system2.NewDictionaryUsecase(dictionaryRepo)
dictionaryService := service.NewDictionaryService(dictionaryUsecase)
dictionary := handler.NewDictionary(dictionaryService)
parameterRepo := system.NewParameterRepo(dataData)
parameterUsecase := biz.NewParameterUsecase(parameterRepo)
parameterUsecase := system2.NewParameterUsecase(parameterRepo)
parameterService := service.NewParameterService(parameterUsecase)
parameter := handler.NewParameter(parameterService)
tokenService := service.NewTokenService(tokenUsecase, tokenIssuer)
apiToken := handler.NewAPIToken(tokenService)
initializationRepo := initialize.NewRepo(dataData, catalog)
systemConfigUsecase := biz.NewSystemConfigUsecase(initializationRepo, taskRuntime)
systemConfigUsecase := system2.NewSystemConfigUsecase(initializationRepo, taskRuntime)
systemConfigService := service.NewSystemConfigService(systemConfigUsecase, runtimeSettings)
systemConfig := handler.NewSystemConfig(systemConfigService, securityService)
public := handler.NewPublic(authService, systemConfigService, securityService)
@ -150,13 +150,14 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
integrationConfigRepo := system.NewIntegrationConfigRepo(dataData)
store := data.NewIntegrationRuntime(dataData)
connectivityTester := integration.NewConnectivityTester(store)
integrationConfigUsecase := biz.NewIntegrationConfigUsecase(integrationConfigRepo, connectivityTester)
integrationConfigUsecase := system2.NewIntegrationConfigUsecase(integrationConfigRepo, connectivityTester)
integrationConfigService := service.NewIntegrationConfigService(integrationConfigUsecase)
integrationConfig := handler.NewIntegrationConfig(integrationConfigService)
v := handler.NewSet(authority, menu, api, permission, organization, announcement, handlerEmail, handlerPayment, task, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session, integrationConfig)
routes := router.NewRoutes(v)
taskMethods := worker.NewTaskMethods(taskUsecase, mediaUsecase, runtime)
moduleRuntime := app.Runtime(routes, taskMethods, registry)
appRuntimeContributions := runtimeContributions(routes, taskMethods)
moduleRuntime := app.Runtime(appRuntimeContributions, registry)
websocketServer, cleanup2, err := websocket.New(store)
if err != nil {
cleanup()

View File

@ -18,7 +18,9 @@
## system 内部保留边界
- `app`:组合根,绑定 system 的迁移、菜单、路由和任务贡献。
- `app`:组合根,汇总各模块的迁移、菜单、路由和任务贡献。
- `modules/system`system 模块的 Definition声明迁移、管理面和默认任务。
- `modules/payment`payment 模块的 Definition声明支付迁移和支付管理面。
- `biz`:用户、权限、菜单、审计、任务、支付订单和系统配置等领域模型与用例。
- `conf`system 配置 proto、运行时快照和生成代码。
- `data`数据库连接、PO、仓储、system 表、支付持久化和配置 watcher。
@ -36,7 +38,8 @@
```text
internal/
app/ # 组合根和模块定义
app/ # 组合根和 catalog
modules/ # 业务模块定义及其模块级贡献
biz/ # DO、usecase、repo interface
conf/ # 配置 proto/runtime
data/ # PO、repo、数据库和迁移

View File

@ -1,6 +1,6 @@
# `internal` 目录结构优化结论
参考 Go Kratos 的分层方式,顶层保留 `app`、`biz`、`conf`、`data`、
参考 Go Kratos 的分层方式,顶层保留 `app`、`modules`、`biz`、`conf`、`data`、
`initialize`、`integration`、`security`、`server`、`service`、`worker` 十个
稳定职责。目录不是越少越好:同一技术角色文件较多时,应在所属层下分组,避免
一个目录堆积几十个文件。
@ -9,7 +9,9 @@
```text
internal/
app/ # 组合根、模块 catalog 和 definition
app/ # 组合根、模块 catalog 和运行时组合
modules/system/ # system 模块定义
modules/payment/ # payment 模块定义
biz/ # DO、usecase、repo interface
conf/ # 配置 proto/runtime
data/ # PO、repo、数据库和迁移
@ -38,12 +40,15 @@ internal/
独立边界时不继续拆分。
- 删除只转发 `pkg/protoutil``utils/configutil`
## `internal/app` 为什么只有两个文件
## `internal/app` 为什么只保留组合代码
`app/catalog.go``app/definition.go` 是有意保留的组合根。`catalog.go` 负责
组装模块、任务注册与运行时;`definition.go` 声明 system 提供的迁移、管理面
和默认定时任务。它们依赖多个层,只能位于应用组合边界,不能并入 `biz`
`service``data`
`app/catalog.go` 是有意保留的组合根,负责组装模块、任务注册与运行时。
system 自身的迁移、管理面和默认定时任务位于
`modules/system/definition.go`,由模块包声明后再被 catalog 汇总。这样模块
定义不再和应用组合逻辑混在一起,也不能误并入 `biz`、`service` 或 `data`
Catalog 只能自动汇总静态模块贡献;新增模块若提供运行时路由或依赖型任务,仍需
在 cmd/Wire 中显式注册,直到统一的 runtime contribution 协议落地。
## 其他目录审查

View File

@ -3,7 +3,8 @@
系统模块承载当前管理后台的完整业务边界。`internal` 顶层只保留有明确
生命周期或分层职责的包:
- `app`组合根、system 模块定义和任务/路由运行时组合
- `app`:组合根、模块 catalog 和任务/路由运行时组合
- `modules`:按业务模块维护 Definition 等模块贡献
- `biz`:系统领域对象、用例和仓储接口
- `conf`:基础配置 proto 与运行时配置解析
- `data`:数据库生命周期、系统仓储、系统表和支付持久化
@ -18,18 +19,19 @@
目录代表边界模块文件按资源命名。DTO、handler、中间件、路由和 HTTP
响应工具分别放在独立子包中,避免 `service`/`server` 根目录堆积几十个
文件,同时不把只有一两个文件的业务逻辑再拆成新包。单文件的 module
定义并入 `app`JWT 实现集中在 `security`protobuf JSON 统一使用
文件,同时不把只有一两个文件的业务逻辑再拆成新包。system 的 module
定义位于 `modules/system`JWT 实现集中在 `security`protobuf JSON 统一使用
`pkg/protoutil`
`internal/app``catalog.go``definition.go` 是有意保留的组合根:
前者组装模块 catalog、任务注册和运行时后者声明 system 的迁移、管理面
和定时任务。它们不是可以下沉到 `service``data` 的业务文件
`internal/app`保留 `catalog.go` 作为组合根:它负责组装模块 catalog、任务
注册和运行时。system 的迁移、管理面和定时任务由 `internal/modules/system`
自己的 `Definition()` 声明,便于后续业务模块独立接入
系统表统一使用 `sys_` 前缀;业务表应由新业务模块自行命名和迁移,不要混入
本目录。
system 通过 `Definition()` 提供迁移、支付菜单/API 和默认任务,通过
system 通过 `modules/system.Definition()` 提供系统迁移、通信集成菜单/API 和默认任务;
payment 通过 `modules/payment.Definition()` 提供支付迁移及支付菜单/API。两者通过
`worker.TaskMethods` 提供依赖系统用例的任务实现,通过 `server/router.Routes` 提供
路由。应用组合根消费这些公共协议;新增业务不需要修改 system 的初始化、
worker、路由或数据层
路由。静态模块贡献可由 catalog 汇总;带运行时依赖的路由和任务仍需在 cmd/Wire
中显式装配,不应误认为只添加 Definition 就能自动发现

View File

@ -4,17 +4,21 @@
package app
import (
systemrouter "kra/internal/server/router"
systemworker "kra/internal/worker"
paymentmodule "kra/internal/modules/payment"
systemmodule "kra/internal/modules/system"
"kra/pkg/module"
platformtask "kra/pkg/task"
)
// Catalog lists the business modules enabled in this binary. Adding an order
// module means adding one Definition here; system initialization and runtime
// code consume the catalog without knowing that module's implementation.
// Catalog lists the business modules enabled in this binary. Static module
// contributions (migrations, admin metadata, default tasks) enter through a
// Definition; runtime routes and dependency-bearing task contributors still
// need explicit wiring below.
func Catalog() module.Catalog {
return module.Catalog{Definitions: []module.Definition{Definition()}}
return module.Catalog{Definitions: []module.Definition{
systemmodule.Definition(),
paymentmodule.Definition(),
}}
}
// TaskRegistry builds the process-wide registry from dependency-free module
@ -26,8 +30,15 @@ func TaskRegistry(catalog module.Catalog) *platformtask.Registry {
return registry
}
// Runtime composes HTTP route contributors from the enabled modules.
func Runtime(systemRoutes *systemrouter.Routes, systemTasks *systemworker.TaskMethods, registry *platformtask.Registry) *module.Runtime {
platformtask.Apply(registry, systemTasks)
return module.NewRuntime(systemRoutes)
// RuntimeContributions groups runtime objects that need constructed
// dependencies. The binary composition root supplies the concrete modules.
type RuntimeContributions struct {
Routes []module.RouteRegistrar
Tasks []platformtask.Contributor
}
// Runtime activates dependency-bearing tasks and composes module routes.
func Runtime(contributions RuntimeContributions, registry *platformtask.Registry) *module.Runtime {
platformtask.Apply(registry, contributions.Tasks...)
return module.NewRuntime(contributions.Routes...)
}

View File

@ -0,0 +1,74 @@
package app
import (
"context"
"encoding/json"
"testing"
"github.com/gin-gonic/gin"
"kra/pkg/module"
platformtask "kra/pkg/task"
)
func TestCatalogIncludesSystemDefinition(t *testing.T) {
catalog := Catalog()
if len(catalog.Definitions) != 2 {
t.Fatalf("definitions = %d, want 2", len(catalog.Definitions))
}
if catalog.Definitions[0].Name != "system" || catalog.Definitions[1].Name != "payment" {
t.Fatalf("definition order = [%q, %q], want [system, payment]", catalog.Definitions[0].Name, catalog.Definitions[1].Name)
}
if got := catalog.MigrationSteps(); len(got) != 5 {
t.Fatalf("module migrations = %d, want 5", len(got))
}
if surface := catalog.Surface(); len(surface.Menus) != 3 || len(surface.APIs) != 15 {
t.Fatalf("admin surface = %d menus/%d APIs, want 3/15", len(surface.Menus), len(surface.APIs))
}
if got := catalog.DefaultTimedTasks(); len(got) != 2 {
t.Fatalf("default timed tasks = %d, want 2", len(got))
}
}
func TestTaskRegistryRegistersStaticModuleMethods(t *testing.T) {
method := platformtask.Method{
Name: "test.static",
Run: func(context.Context, json.RawMessage) error { return nil },
}
catalog := module.Catalog{Definitions: []module.Definition{{Tasks: []platformtask.Method{method}}}}
registry := TaskRegistry(catalog)
if _, ok := registry.Lookup(method.Name); !ok {
t.Fatalf("method %q was not registered", method.Name)
}
}
type testRouteRegistrar struct{ called bool }
func (registrar *testRouteRegistrar) RegisterRoutes(*gin.RouterGroup, *gin.RouterGroup, *gin.Engine) {
registrar.called = true
}
type testTaskContributor struct{ name string }
func (contributor testTaskContributor) RegisterTasks(registry *platformtask.Registry) {
registry.Register(platformtask.Method{
Name: contributor.name,
Run: func(context.Context, json.RawMessage) error { return nil },
})
}
func TestRuntimeAppliesAllContributions(t *testing.T) {
registry := platformtask.NewRegistry()
route := &testRouteRegistrar{}
runtime := Runtime(RuntimeContributions{
Routes: []module.RouteRegistrar{route},
Tasks: []platformtask.Contributor{testTaskContributor{name: "test.runtime"}},
}, registry)
if _, ok := registry.Lookup("test.runtime"); !ok {
t.Fatal("runtime task contributor was not applied")
}
runtime.RegisterRoutes(nil, nil, nil)
if !route.called {
t.Fatal("runtime route contributor was not called")
}
}

View File

@ -1,6 +1,10 @@
package biz
import "github.com/google/wire"
import (
"kra/internal/biz/system"
"github.com/google/wire"
)
// ProviderSet is biz providers.
var ProviderSet = wire.NewSet(NewUserUsecase, NewAuthenticationUsecase, NewSystemConfigUsecase, NewAuthorityUsecase, NewAPIUsecase, NewPermissionUsecase, NewAccessControlUsecase, NewMenuUsecase, NewDepartmentUsecase, NewPositionUsecase, NewDictionaryUsecase, NewParameterUsecase, NewTokenUsecase, NewSecurityUsecase, NewVersionUsecase, NewExportUsecase, NewAuditUsecase, NewAuditRecorderUsecase, NewLogViewerUsecase, NewTaskUsecaseWithRegistry, NewTaskApplicationUsecase, NewMediaUsecase, NewAnnouncementUsecase, NewEmailUsecase, NewPaymentUsecase, NewIntegrationConfigUsecase)
var ProviderSet = wire.NewSet(system.NewUserUsecase, system.NewAuthenticationUsecase, system.NewSystemConfigUsecase, system.NewAuthorityUsecase, system.NewAPIUsecase, system.NewPermissionUsecase, system.NewAccessControlUsecase, system.NewMenuUsecase, system.NewDepartmentUsecase, system.NewPositionUsecase, system.NewDictionaryUsecase, system.NewParameterUsecase, system.NewTokenUsecase, system.NewSecurityUsecase, system.NewVersionUsecase, system.NewExportUsecase, system.NewAuditUsecase, system.NewAuditRecorderUsecase, system.NewLogViewerUsecase, system.NewTaskUsecaseWithRegistry, system.NewTaskApplicationUsecase, system.NewMediaUsecase, system.NewAnnouncementUsecase, system.NewEmailUsecase, system.NewPaymentUsecase, system.NewIntegrationConfigUsecase)

View File

@ -1,4 +1,4 @@
package biz
package system
import "context"

View File

@ -1,4 +1,4 @@
package biz
package system
import "context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import "context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"github.com/go-kratos/kratos/v3/errors"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
func integrationField(key, label string, required, secret bool, fieldType string) IntegrationConfigField {
if fieldType == "" {

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"bufio"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"go.einride.tech/aip/filtering"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import "context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import platformtask "kra/pkg/task"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -1,4 +1,4 @@
package biz
package system
import (
"context"

View File

@ -3,12 +3,11 @@ package data
import (
"database/sql"
"errors"
"kra/internal/biz/system"
"log/slog"
"reflect"
"strings"
"kra/internal/biz"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"gorm.io/gorm/schema"
@ -64,7 +63,7 @@ func skipDataScope(db *gorm.DB) bool {
return ok && value
}
func validDataScope(scope biz.DataScope) bool {
func validDataScope(scope system.DataScope) bool {
return scope.UserID != 0 && scope.AuthorityID != 0 && scope.Scope >= 1 && scope.Scope <= 5 && scope.All == (scope.Scope == 1)
}
@ -79,10 +78,10 @@ func applyDataScope(operation string, enqueue dataScopeAuditEnqueue) func(*gorm.
if skipDataScope(db) {
return
}
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
scope, ok := system.DataScopeFromContext(db.Statement.Context)
if !ok {
slog.WarnContext(db.Statement.Context, "数据权限: 业务表访问无身份上下文, 已拒绝", "mod", "data-scope", "table", db.Statement.Table)
recordDataScopeEvent(db, enqueue, "no_identity", operation, "无身份上下文访问受控表, 已拒绝", biz.DataScope{})
recordDataScopeEvent(db, enqueue, "no_identity", operation, "无身份上下文访问受控表, 已拒绝", system.DataScope{})
_ = db.AddError(errDataScopeRequired)
return
}
@ -116,7 +115,7 @@ func applyDataScope(operation string, enqueue dataScopeAuditEnqueue) func(*gorm.
}
}
func recordDataScopeEvent(db *gorm.DB, enqueue dataScopeAuditEnqueue, eventType, operation, detail string, scope biz.DataScope) {
func recordDataScopeEvent(db *gorm.DB, enqueue dataScopeAuditEnqueue, eventType, operation, detail string, scope system.DataScope) {
if enqueue == nil {
return
}
@ -128,7 +127,7 @@ func auditBlockedWrite(operation string, enqueue dataScopeAuditEnqueue) func(*go
if _, applied := db.Statement.Clauses["data_scope:applied"]; !applied || db.Error != nil || db.RowsAffected != 0 {
return
}
if scope, ok := biz.DataScopeFromContext(db.Statement.Context); ok && !scope.All {
if scope, ok := system.DataScopeFromContext(db.Statement.Context); ok && !scope.All {
recordDataScopeEvent(db, enqueue, "blocked_write", operation, "数据范围过滤后写操作影响 0 行(疑似越权尝试)", scope)
}
}
@ -139,9 +138,9 @@ func stampOwnership(enqueue dataScopeAuditEnqueue) func(*gorm.DB) {
if !isControlledTable(db) || skipDataScope(db) {
return
}
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
scope, ok := system.DataScopeFromContext(db.Statement.Context)
if !ok {
recordDataScopeEvent(db, enqueue, "no_identity", "create", "无身份上下文访问受控表, 已拒绝", biz.DataScope{})
recordDataScopeEvent(db, enqueue, "no_identity", "create", "无身份上下文访问受控表, 已拒绝", system.DataScope{})
_ = db.AddError(errDataScopeRequired)
return
}
@ -164,7 +163,7 @@ func stampUpdatedBy(db *gorm.DB) {
if !isControlledTable(db) || stmt.SkipHooks || !hasScopeField(db, "updated_by") {
return
}
scope, ok := biz.DataScopeFromContext(db.Statement.Context)
scope, ok := system.DataScopeFromContext(db.Statement.Context)
if !ok || scope.UserID == 0 {
return
}
@ -212,7 +211,7 @@ func stampDeletedBy(db *gorm.DB) {
if _, customZero := deletedAt.TagSettings["ZEROVALUE"]; customZero {
return
}
scope, ok := biz.DataScopeFromContext(stmt.Context)
scope, ok := system.DataScopeFromContext(stmt.Context)
if !ok || scope.UserID == 0 {
return
}

View File

@ -3,10 +3,9 @@ package data
import (
"context"
"errors"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
"gorm.io/gorm"
)
@ -53,12 +52,12 @@ func TestDataScopeCallbacksFailClosedAndAllowExplicitSystemBypass(t *testing.T)
t.Fatalf("query without data scope error = %v", err)
}
invalidCtx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 1, All: false})
invalidCtx := system.NewDataScopeContext(context.Background(), system.DataScope{UserID: 7, AuthorityID: 1, Scope: 1, All: false})
if err := db.WithContext(invalidCtx).Find(&rows).Error; !errors.Is(err, errInvalidDataScope) {
t.Fatalf("query with invalid data scope error = %v", err)
}
scopedCtx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 3, DepartmentIDs: []uint{10}})
scopedCtx := system.NewDataScopeContext(context.Background(), system.DataScope{UserID: 7, AuthorityID: 1, Scope: 3, DepartmentIDs: []uint{10}})
rows = nil
if err := db.WithContext(scopedCtx).Order("id").Find(&rows).Error; err != nil {
t.Fatal(err)
@ -67,7 +66,7 @@ func TestDataScopeCallbacksFailClosedAndAllowExplicitSystemBypass(t *testing.T)
t.Fatalf("scoped query rows = %+v", rows)
}
emptyCtx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 5})
emptyCtx := system.NewDataScopeContext(context.Background(), system.DataScope{UserID: 7, AuthorityID: 1, Scope: 5})
rows = nil
if err := db.WithContext(emptyCtx).Find(&rows).Error; err != nil {
t.Fatal(err)
@ -91,7 +90,7 @@ func TestDataScopeCreateRequiresIdentityAndStampsOwnership(t *testing.T) {
t.Fatalf("create without data scope error = %v", err)
}
ctx := biz.NewDataScopeContext(context.Background(), biz.DataScope{UserID: 7, AuthorityID: 1, Scope: 3, PrimaryDeptID: 10, DepartmentIDs: []uint{10}})
ctx := system.NewDataScopeContext(context.Background(), system.DataScope{UserID: 7, AuthorityID: 1, Scope: 3, PrimaryDeptID: 10, DepartmentIDs: []uint{10}})
created := dataScopeRecord{Name: "owned", DeptID: 999, CreatedBy: 999}
if err := db.WithContext(ctx).Create(&created).Error; err != nil {
t.Fatal(err)

View File

@ -4,8 +4,8 @@ import (
"context"
"errors"
"fmt"
"kra/internal/biz/system"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/integration/storage"
@ -139,7 +139,7 @@ func (d *Data) IsInitialized(context.Context) (bool, error) {
// InitializeDatabase opens and activates the configured database. The callback
// is the application-level first-install hook; data owns only lifecycle and
// schema migration, while initialize owns system seed orchestration.
func (d *Data) InitializeDatabase(ctx context.Context, input *biz.DatabaseConfig, seed func(context.Context, *gorm.DB) error) error {
func (d *Data) InitializeDatabase(ctx context.Context, input *system.DatabaseConfig, seed func(context.Context, *gorm.DB) error) error {
config := &conf.Data_Database{}
if current := d.runtime.Data(); current != nil && current.Database != nil {
config = proto.Clone(current.Database).(*conf.Data_Database)

View File

@ -3,8 +3,7 @@ package data
import (
"encoding/json"
"errors"
"kra/internal/biz"
"kra/internal/biz/system"
"gorm.io/gorm"
)
@ -14,15 +13,15 @@ func ensureCommunicationIntegrationConfigs(db *gorm.DB) error {
kind string
provider string
}{
{kind: biz.IntegrationKindMQ, provider: "emqx"},
{kind: biz.IntegrationKindMQ, provider: "rabbitmq"},
{kind: biz.IntegrationKindWebSocket, provider: "melody"},
{kind: system.IntegrationKindMQ, provider: "emqx"},
{kind: system.IntegrationKindMQ, provider: "rabbitmq"},
{kind: system.IntegrationKindWebSocket, provider: "melody"},
}
for _, item := range defaults {
var row integrationConfigPO
err := db.Where("kind = ? AND provider = ?", item.kind, item.provider).First(&row).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
values, marshalErr := json.Marshal(biz.DefaultIntegrationConfig(item.kind, item.provider))
values, marshalErr := json.Marshal(system.DefaultIntegrationConfig(item.kind, item.provider))
if marshalErr != nil {
return marshalErr
}

View File

@ -2,7 +2,6 @@ package payment
import (
"kra/pkg/database/migration"
platformmodule "kra/pkg/module"
"gorm.io/gorm"
)
@ -15,27 +14,3 @@ func Migrations() []migration.Step {
{ID: "202608200004_payment_defaults", Migrate: ensurePaymentIntegrationConfigs},
}
}
// AdminSurface describes the payment-owned entries shown in the system
// administration UI. The system module persists these records because it owns
// the menu/API/policy tables.
func AdminSurface() platformmodule.Surface {
return platformmodule.Surface{
Menus: []platformmodule.Menu{
{Name: "paymentOrders", Path: "paymentOrders", ParentName: "extensions", Component: "view/systemTools/payment/orders.vue", Title: "支付订单", Icon: "wallet", Sort: 6},
{Name: "paymentConfig", Path: "paymentConfig", ParentName: "extensions", Component: "view/systemTools/payment/config.vue", Title: "支付配置", Icon: "credit-card", Sort: 7},
},
APIs: []platformmodule.API{
{Path: "/payment/orders", Method: "GET", Group: "支付", Description: "分页查询支付订单"},
{Path: "/payment/order", Method: "POST", Group: "支付", Description: "查询支付订单"},
{Path: "/payment/orders/:provider/:tradeNo", Method: "GET", Group: "支付", Description: "按路径查询支付订单"},
{Path: "/payment/create", Method: "POST", Group: "支付", Description: "创建支付订单"},
{Path: "/payment/query", Method: "POST", Group: "支付", Description: "同步支付订单状态"},
{Path: "/payment/refund", Method: "POST", Group: "支付", Description: "申请支付订单退款"},
{Path: "/payment/orders/:provider/:tradeNo/refund", Method: "POST", Group: "支付", Description: "按路径申请支付订单退款"},
{Path: "/payment/fulfill", Method: "POST", Group: "支付", Description: "重试支付订单发货"},
{Path: "/payment/orders/:provider/:tradeNo/fulfill", Method: "POST", Group: "支付", Description: "按路径重试支付订单发货"},
{Path: "/payment/providers/:provider/test", Method: "POST", Group: "支付", Description: "测试支付渠道"},
},
}
}

View File

@ -7,12 +7,12 @@ import (
"encoding/json"
"errors"
"fmt"
"kra/internal/biz/system"
"net/url"
"strconv"
"strings"
"time"
"kra/internal/biz"
datapayment "kra/internal/integration/payment"
"github.com/google/uuid"
@ -21,13 +21,13 @@ import (
type paymentRepo struct{ data Provider }
func NewPaymentRepo(data Provider) biz.PaymentRepo { return &paymentRepo{data: data} }
func NewPaymentRepo(data Provider) system.PaymentRepo { return &paymentRepo{data: data} }
func ensurePaymentIntegrationConfigs(db *gorm.DB) error {
for _, provider := range biz.SupportedPaymentProviders {
for _, provider := range system.SupportedPaymentProviders {
var row integrationConfigPO
err := db.Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error
defaults := biz.DefaultIntegrationConfig(integrationKindPayment, provider)
defaults := system.DefaultIntegrationConfig(integrationKindPayment, provider)
if errors.Is(err, gorm.ErrRecordNotFound) {
encoded, _ := json.Marshal(defaults)
if err := db.Create(&integrationConfigPO{Kind: integrationKindPayment, Provider: provider, Enabled: false, Config: string(encoded)}).Error; err != nil {
@ -61,7 +61,7 @@ func (r *paymentRepo) row(ctx context.Context, provider string) (*integrationCon
var row integrationConfigPO
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil, biz.ErrPaymentProviderNotFound
return nil, nil, system.ErrPaymentProviderNotFound
}
return nil, nil, err
}
@ -84,11 +84,11 @@ func (r *paymentRepo) adapter(ctx context.Context, provider string) (datapayment
return adapter, values, err
}
func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.PaymentTestResult, error) {
func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*system.PaymentTestResult, error) {
started := time.Now()
test := &biz.PaymentTestResult{Provider: provider, TradeNo: "", Passed: false, Stages: []biz.PaymentTestStage{}}
test := &system.PaymentTestResult{Provider: provider, TradeNo: "", Passed: false, Stages: []system.PaymentTestStage{}}
add := func(name, status, message, tradeNo string, since time.Time) {
test.Stages = append(test.Stages, biz.PaymentTestStage{Name: name, Status: status, Message: message, TradeNo: tradeNo, Duration: time.Since(since).Milliseconds()})
test.Stages = append(test.Stages, system.PaymentTestStage{Name: name, Status: status, Message: message, TradeNo: tradeNo, Duration: time.Since(since).Milliseconds()})
}
values, err := r.testRow(ctx, provider)
if err != nil {
@ -97,7 +97,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
}
test.Mode = strings.ToLower(strings.TrimSpace(text(values, "environment")))
configStart := time.Now()
if err = biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, provider, values); err != nil {
if err = system.ValidateIntegrationConfig(system.IntegrationKindPayment, provider, values); err != nil {
add("config", "failed", err.Error(), "", configStart)
return test, err
}
@ -117,11 +117,11 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
orders := &paymentOrderRepo{data: r.data}
extra, _ := json.Marshal(req.Extra)
localStart := time.Now()
order, _, err := orders.CreatePaymentOrder(ctx, &biz.PaymentOrder{
order, _, err := orders.CreatePaymentOrder(ctx, &system.PaymentOrder{
TradeNo: req.TradeNo, Provider: provider, BusinessType: req.BusinessType, BusinessID: req.BusinessID,
Subject: req.Subject, PaymentMode: biz.PaymentModeExternal, OriginalAmount: req.Amount, Amount: req.Amount,
Currency: req.Currency, PaymentStatus: biz.PaymentStatusInitialized, FulfillmentStatus: biz.FulfillmentStatusPending,
RefundStatus: biz.RefundStatusNone, ConfirmationID: uuid.NewString(), RequestFingerprint: paymentTestFingerprint(req), Extra: extra,
Subject: req.Subject, PaymentMode: system.PaymentModeExternal, OriginalAmount: req.Amount, Amount: req.Amount,
Currency: req.Currency, PaymentStatus: system.PaymentStatusInitialized, FulfillmentStatus: system.FulfillmentStatusPending,
RefundStatus: system.RefundStatusNone, ConfirmationID: uuid.NewString(), RequestFingerprint: paymentTestFingerprint(req), Extra: extra,
})
if err != nil {
add("local_order", "failed", err.Error(), req.TradeNo, localStart)
@ -159,7 +159,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
if queryID == "" {
queryID = req.TradeNo
}
if provider == biz.PaymentApple {
if provider == system.PaymentApple {
queryID = strings.TrimSpace(text(values, "test_transaction_id"))
if queryID == "" {
err = errors.New("Apple 连通性测试需要配置 test_transaction_id沙箱交易 ID")
@ -179,7 +179,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
add("query", "failed", err.Error(), req.TradeNo, queryStart)
return test, err
}
if provider != biz.PaymentApple {
if provider != system.PaymentApple {
if order, err = orders.ApplyPaymentResult(ctx, provider, req.TradeNo, paymentTestProviderUpdate(queried)); err != nil {
add("local_order", "failed", "回写测试查单结果失败: "+err.Error(), req.TradeNo, queryStart)
return test, err
@ -193,9 +193,9 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
test.Result = queried
add("query", "passed", "测试订单查询成功,状态: "+queried.Status, req.TradeNo, queryStart)
if queried.Status != "success" || provider == biz.PaymentApple {
if queried.Status != "success" || provider == system.PaymentApple {
message := "订单尚未支付成功,已完成配置、下单和查单连通性测试;请在沙箱完成付款后重试"
if provider == biz.PaymentApple {
if provider == system.PaymentApple {
message = "Apple 退款由 App Store 管理,已完成配置、下单和交易查询测试"
}
add("refund", "skipped", message, req.TradeNo, time.Now())
@ -208,7 +208,7 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
add("refund", "failed", beginErr.Error(), req.TradeNo, refundStart)
return test, beginErr
}
refund, refundErr := adapter.Refund(ctx, &biz.PaymentRefundRequest{Provider: provider, TradeNo: req.TradeNo, ProviderTradeNo: order.ProviderTradeNo, QueryID: order.QueryID, RefundNo: order.RefundNo, Amount: req.Amount, TotalAmount: req.Amount, Currency: req.Currency}, values)
refund, refundErr := adapter.Refund(ctx, &system.PaymentRefundRequest{Provider: provider, TradeNo: req.TradeNo, ProviderTradeNo: order.ProviderTradeNo, QueryID: order.QueryID, RefundNo: order.RefundNo, Amount: req.Amount, TotalAmount: req.Amount, Currency: req.Currency}, values)
if refundErr != nil {
recordPaymentTestError(ctx, r.data, provider, req.TradeNo, refundErr)
add("refund", "failed", refundErr.Error(), req.TradeNo, refundStart)
@ -230,17 +230,17 @@ func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.P
return test, nil
}
func paymentTestFingerprint(req *biz.PaymentRequest) string {
func paymentTestFingerprint(req *system.PaymentRequest) string {
raw, _ := json.Marshal(req)
hash := sha256.Sum256(raw)
return hex.EncodeToString(hash[:])
}
func paymentTestProviderUpdate(result *biz.PaymentResult) *biz.PaymentProviderUpdate {
func paymentTestProviderUpdate(result *system.PaymentResult) *system.PaymentProviderUpdate {
if result == nil {
return nil
}
return &biz.PaymentProviderUpdate{
return &system.PaymentProviderUpdate{
Status: result.Status, ProviderStatus: result.Status, ProviderTradeNo: result.ProviderTradeNo, QueryID: result.QueryID,
Amount: result.Amount, PayerPaidAmount: result.PayerPaidAmount, CashPaidAmount: result.CashPaidAmount,
PointPaidAmount: result.PointPaidAmount, DiscountAmount: result.DiscountAmount,
@ -250,21 +250,21 @@ func paymentTestProviderUpdate(result *biz.PaymentResult) *biz.PaymentProviderUp
}
}
func validatePaymentTestResult(provider, tradeNo string, result *biz.PaymentResult) error {
func validatePaymentTestResult(provider, tradeNo string, result *system.PaymentResult) error {
if result == nil {
return errors.New("支付渠道响应为空")
}
if strings.TrimSpace(result.Provider) != provider {
return errors.New("支付渠道响应的 provider 不匹配")
}
if value := strings.TrimSpace(result.TradeNo); provider != biz.PaymentApple && value != "" && value != tradeNo {
if value := strings.TrimSpace(result.TradeNo); provider != system.PaymentApple && value != "" && value != tradeNo {
return errors.New("支付渠道响应的商户订单号不匹配")
}
return nil
}
func queryPaymentTest(ctx context.Context, adapter datapayment.Adapter, queryID string, values map[string]any) (*biz.PaymentResult, error) {
var result *biz.PaymentResult
func queryPaymentTest(ctx context.Context, adapter datapayment.Adapter, queryID string, values map[string]any) (*system.PaymentResult, error) {
var result *system.PaymentResult
var err error
for attempt := 0; attempt < 3; attempt++ {
result, err = adapter.Query(ctx, queryID, values)
@ -300,7 +300,7 @@ func (r *paymentRepo) testRow(ctx context.Context, provider string) (map[string]
var row integrationConfigPO
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrPaymentProviderNotFound
return nil, system.ErrPaymentProviderNotFound
}
return nil, err
}
@ -311,17 +311,17 @@ func (r *paymentRepo) testRow(ctx context.Context, provider string) (map[string]
return values, nil
}
func paymentTestRequest(provider string, values map[string]any) *biz.PaymentRequest {
func paymentTestRequest(provider string, values map[string]any) *system.PaymentRequest {
tradeNo := "kra-test-" + time.Now().UTC().Format("20060102150405.000000000")
amount := configuredInt64(values, "test_amount", 1)
if amount <= 0 {
amount = 1
}
req := &biz.PaymentRequest{Provider: provider, TradeNo: strings.ReplaceAll(tradeNo, ".", ""), Subject: "Kra 支付渠道连通性测试", Amount: amount, Currency: strings.ToUpper(firstAny(values, "test_currency", "currency", "fee_type")), NotifyURL: text(values, "notify_url"), ReturnURL: text(values, "return_url"), BusinessType: "system_payment_test", BusinessID: uuid.NewString(), Extra: map[string]any{}}
req := &system.PaymentRequest{Provider: provider, TradeNo: strings.ReplaceAll(tradeNo, ".", ""), Subject: "Kra 支付渠道连通性测试", Amount: amount, Currency: strings.ToUpper(firstAny(values, "test_currency", "currency", "fee_type")), NotifyURL: text(values, "notify_url"), ReturnURL: text(values, "return_url"), BusinessType: "system_payment_test", BusinessID: uuid.NewString(), Extra: map[string]any{}}
if req.Currency == "" {
req.Currency = "CNY"
}
if provider == biz.PaymentApple {
if provider == system.PaymentApple {
req.TradeNo = uuid.NewString()
req.Extra["product_id"] = firstAny(values, "product_id", "test_product_id")
}
@ -351,10 +351,10 @@ func validatePaymentTestSettings(provider string, values map[string]any) error {
return fmt.Errorf("test_extra 必须是 JSON 对象: %w", err)
}
}
if provider == biz.PaymentApple && strings.TrimSpace(text(values, "test_transaction_id")) == "" {
if provider == system.PaymentApple && strings.TrimSpace(text(values, "test_transaction_id")) == "" {
return errors.New("Apple 测试需要 test_transaction_id沙箱交易 ID")
}
if provider == biz.PaymentApple && strings.TrimSpace(firstAny(values, "test_product_id", "product_id")) == "" {
if provider == system.PaymentApple && strings.TrimSpace(firstAny(values, "test_product_id", "product_id")) == "" {
return errors.New("Apple 测试需要 test_product_id沙箱商品 ID")
}
return nil
@ -377,7 +377,7 @@ func testModeEnabled(values map[string]any) bool {
}
}
func (r *paymentRepo) Create(ctx context.Context, req *biz.PaymentRequest) (*biz.PaymentResult, error) {
func (r *paymentRepo) Create(ctx context.Context, req *system.PaymentRequest) (*system.PaymentResult, error) {
if req == nil {
return nil, errors.New("支付下单请求为空")
}
@ -396,7 +396,7 @@ func (r *paymentRepo) Create(ctx context.Context, req *biz.PaymentRequest) (*biz
func paymentProviderRequiresNotifyURL(provider string) bool {
switch provider {
case biz.PaymentApple, biz.PaymentAllinPay, biz.PaymentSaobei, biz.PaymentPayPal:
case system.PaymentApple, system.PaymentAllinPay, system.PaymentSaobei, system.PaymentPayPal:
return false
default:
return true
@ -413,15 +413,15 @@ func paymentCreateRequiresNotifyURL(provider string, extra, config map[string]an
}
keys := []string{"method", "pay_method", "trade_type", "pay_type", "channel"}
switch provider {
case biz.PaymentAlipay, biz.PaymentAlipayV3:
case system.PaymentAlipay, system.PaymentAlipayV3:
keys = []string{"method", "pay_method", "trade_type", "channel"}
case biz.PaymentWechatV2:
case system.PaymentWechatV2:
keys = []string{"trade_type", "pay_type", "method", "pay_method", "channel"}
case biz.PaymentWechatV3:
case system.PaymentWechatV3:
keys = []string{"trade_type", "pay_type", "method"}
case biz.PaymentQQ:
case system.PaymentQQ:
keys = []string{"trade_type", "pay_type", "method", "pay_method"}
case biz.PaymentLakala:
case system.PaymentLakala:
keys = []string{"method", "pay_method", "trade_type"}
}
value := firstAny(extra, keys...)
@ -430,28 +430,28 @@ func paymentCreateRequiresNotifyURL(provider string, extra, config map[string]an
}
normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
switch provider {
case biz.PaymentAlipay, biz.PaymentAlipayV3:
case system.PaymentAlipay, system.PaymentAlipayV3:
return !contains([]string{"pay", "trade_pay", "alipay_trade_pay", "barcode", "barcode_pay", "micropay", "face_to_face"}, normalized)
case biz.PaymentWechatV2:
case system.PaymentWechatV2:
return !contains([]string{"micropay", "micro_pay", "barcode", "barcode_pay", "pay_code", "payment_code"}, normalized)
case biz.PaymentWechatV3:
case system.PaymentWechatV3:
return !contains([]string{"micropay", "micro_pay", "codepay", "code_pay", "barcode", "barcode_pay", "facepay", "face_pay"}, normalized)
case biz.PaymentQQ:
case system.PaymentQQ:
return !contains([]string{"micropay", "micro_pay", "barcode", "barcode_pay"}, normalized)
case biz.PaymentLakala:
case system.PaymentLakala:
return !contains([]string{"retail", "retail_pay", "micropay", "barcode"}, normalized)
default:
return true
}
}
func (r *paymentRepo) Query(ctx context.Context, provider, tradeNo string) (*biz.PaymentResult, error) {
func (r *paymentRepo) Query(ctx context.Context, provider, tradeNo string) (*system.PaymentResult, error) {
a, c, err := r.adapter(ctx, provider)
if err != nil {
return nil, err
}
return a.Query(ctx, tradeNo, c)
}
func (r *paymentRepo) Refund(ctx context.Context, req *biz.PaymentRefundRequest) (*biz.PaymentResult, error) {
func (r *paymentRepo) Refund(ctx context.Context, req *system.PaymentRefundRequest) (*system.PaymentResult, error) {
if req == nil {
return nil, errors.New("支付退款请求为空")
}
@ -461,7 +461,7 @@ func (r *paymentRepo) Refund(ctx context.Context, req *biz.PaymentRefundRequest)
}
return a.Refund(ctx, req, c)
}
func (r *paymentRepo) HandleCallback(ctx context.Context, callback *biz.PaymentCallback) (*biz.PaymentResult, error) {
func (r *paymentRepo) HandleCallback(ctx context.Context, callback *system.PaymentCallback) (*system.PaymentResult, error) {
if callback == nil {
return nil, errors.New("支付回调为空")
}
@ -471,23 +471,23 @@ func (r *paymentRepo) HandleCallback(ctx context.Context, callback *biz.PaymentC
}
result, err := a.Callback(ctx, callback, c)
if err != nil {
return nil, &biz.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
return nil, &system.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
}
if result == nil {
err = errors.New("支付回调解析结果为空")
return nil, &biz.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
return nil, &system.PaymentCallbackError{Cause: err, Ack: paymentCallbackAck(callback.Provider, c, false)}
}
result.SuccessAck = paymentCallbackAck(callback.Provider, c, true)
result.FailureAck = paymentCallbackAck(callback.Provider, c, false)
if result.Provider != callback.Provider {
err = errors.New("支付回调渠道不匹配")
return nil, &biz.PaymentCallbackError{Cause: err, Ack: result.FailureAck}
return nil, &system.PaymentCallbackError{Cause: err, Ack: result.FailureAck}
}
result.EventID = paymentCallbackEventID(callback, result)
return result, nil
}
func paymentCallbackEventID(callback *biz.PaymentCallback, result *biz.PaymentResult) string {
func paymentCallbackEventID(callback *system.PaymentCallback, result *system.PaymentResult) string {
if result != nil {
if eventID := strings.TrimSpace(result.EventID); eventID != "" {
return eventID
@ -504,8 +504,8 @@ func paymentCallbackEventID(callback *biz.PaymentCallback, result *biz.PaymentRe
return hex.EncodeToString(hash[:])
}
func paymentCallbackAck(provider string, values map[string]any, success bool) biz.PaymentCallbackAck {
ack := biz.DefaultPaymentCallbackAck(provider, success)
func paymentCallbackAck(provider string, values map[string]any, success bool) system.PaymentCallbackAck {
ack := system.DefaultPaymentCallbackAck(provider, success)
prefix := "callback_success_"
if !success {
prefix = "callback_failure_"
@ -524,7 +524,7 @@ func paymentCallbackAck(provider string, values map[string]any, success bool) bi
return ack
}
func callbackFields(callback *biz.PaymentCallback) map[string]string {
func callbackFields(callback *system.PaymentCallback) map[string]string {
fields := map[string]string{}
for key, value := range callback.Query {
fields[key] = value
@ -569,5 +569,5 @@ func contains(values []string, value string) bool {
}
func validatePaymentConfig(provider string, values map[string]any) error {
return biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, provider, values)
return system.ValidateIntegrationConfig(system.IntegrationKindPayment, provider, values)
}

View File

@ -1,10 +1,9 @@
package payment
import (
"kra/internal/biz/system"
"strings"
"testing"
"kra/internal/biz"
)
func TestValidatePaymentConfigRequiresDouyinAppIDWhenEnabled(t *testing.T) {
@ -12,18 +11,18 @@ func TestValidatePaymentConfigRequiresDouyinAppIDWhenEnabled(t *testing.T) {
"merchant_id": "merchant-douyin", "serial_no": "merchant-serial", "api_key": "01234567890123456789012345678901",
"private_key": "merchant-private-key", "platform_cert": "platform-public-key", "platform_serial_no": "platform-serial",
}
err := biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, biz.PaymentDouyin, values)
err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentDouyin, values)
if err == nil || !strings.Contains(err.Error(), "app_id") {
t.Fatalf("missing app_id error = %v", err)
}
values["app_id"] = "douyin-app"
if err = biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, biz.PaymentDouyin, values); err != nil {
if err = system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentDouyin, values); err != nil {
t.Fatalf("valid Douyin configuration rejected: %v", err)
}
}
func TestValidatePaymentConfigAcceptsProviderAliases(t *testing.T) {
err := validatePaymentConfig(biz.PaymentDouyin, map[string]any{
err := validatePaymentConfig(system.PaymentDouyin, map[string]any{
"app_id": "douyin-app", "merchant_id": "merchant-douyin", "serial_no": "merchant-serial", "api_key": "01234567890123456789012345678901",
"private_key": "merchant-private-key", "platform_cert": "platform-public-key", "platform_cert_serial": "platform-serial",
})
@ -37,13 +36,13 @@ func TestValidatePaymentConfigProviderRules(t *testing.T) {
name, provider, want string
values map[string]any
}{
{"allinpay order type", biz.PaymentAllinPay, "reqsn", map[string]any{"cus_id": "customer", "app_id": "app", "private_key": "private-key", "public_key": "public-key", "query_order_type": "payinfo"}},
{"paypal webhook", biz.PaymentPayPal, "webhook_id", map[string]any{"client_id": "client-id", "client_secret": "client-secret"}},
{"wechat v2 refund cert", biz.PaymentWechatV2, "client_cert", map[string]any{"app_id": "app", "merchant_id": "merchant", "mch_key": "key"}},
{"allinpay order type", system.PaymentAllinPay, "reqsn", map[string]any{"cus_id": "customer", "app_id": "app", "private_key": "private-key", "public_key": "public-key", "query_order_type": "payinfo"}},
{"paypal webhook", system.PaymentPayPal, "webhook_id", map[string]any{"client_id": "client-id", "client_secret": "client-secret"}},
{"wechat v2 refund cert", system.PaymentWechatV2, "client_cert", map[string]any{"app_id": "app", "merchant_id": "merchant", "mch_key": "key"}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, test.provider, test.values)
err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, test.provider, test.values)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("error = %v, want %q", err, test.want)
}
@ -52,13 +51,13 @@ func TestValidatePaymentConfigProviderRules(t *testing.T) {
}
func TestValidatePaymentConfigGenericRequiresRuntimeFields(t *testing.T) {
values := biz.DefaultIntegrationConfig(biz.IntegrationKindPayment, biz.PaymentChinaums)
if err := biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, biz.PaymentChinaums, values); err == nil {
values := system.DefaultIntegrationConfig(system.IntegrationKindPayment, system.PaymentChinaums)
if err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentChinaums, values); err == nil {
t.Fatal("empty generic payment config unexpectedly accepted")
}
values["app_id"], values["merchant_id"] = "app", "merchant"
values["create_url"], values["query_url"], values["refund_url"], values["app_key"] = "https://pay.test/create", "https://pay.test/query", "https://pay.test/refund", "secret"
if err := biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, biz.PaymentChinaums, values); err == nil {
if err := system.ValidateIntegrationConfig(system.IntegrationKindPayment, system.PaymentChinaums, values); err == nil {
t.Fatal("generic config with only identity/endpoints unexpectedly accepted")
}
}

View File

@ -1,16 +1,15 @@
package payment
import (
"kra/internal/biz/system"
"strings"
"testing"
"kra/internal/biz"
)
func TestPaymentDefinitionsProvideNonEmptyDefaults(t *testing.T) {
definitions := biz.IntegrationDefinitions(biz.IntegrationKindPayment)
if len(definitions) != len(biz.SupportedPaymentProviders) {
t.Fatalf("payment definitions = %d, want %d", len(definitions), len(biz.SupportedPaymentProviders))
definitions := system.IntegrationDefinitions(system.IntegrationKindPayment)
if len(definitions) != len(system.SupportedPaymentProviders) {
t.Fatalf("payment definitions = %d, want %d", len(definitions), len(system.SupportedPaymentProviders))
}
for _, definition := range definitions {
if definition.Provider == "" || definition.Name == "" || len(definition.Fields) == 0 {

View File

@ -4,14 +4,15 @@ import (
"context"
"encoding/json"
"errors"
"kra/internal/biz/system"
"strings"
"time"
"kra/pkg/database/pagination"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"kra/internal/biz"
"kra/pkg/database/pagination"
)
type paymentOrderPO struct {
@ -67,11 +68,11 @@ func (paymentOrderPO) TableName() string { return "pay_orders" }
type paymentOrderRepo struct{ data Provider }
func NewPaymentOrderRepo(data Provider) biz.PaymentOrderRepo {
func NewPaymentOrderRepo(data Provider) system.PaymentOrderRepo {
return &paymentOrderRepo{data: data}
}
func newPaymentOrderPO(order *biz.PaymentOrder) (*paymentOrderPO, error) {
func newPaymentOrderPO(order *system.PaymentOrder) (*paymentOrderPO, error) {
if order == nil {
return nil, errors.New("支付订单为空")
}
@ -84,16 +85,16 @@ func newPaymentOrderPO(order *biz.PaymentOrder) (*paymentOrderPO, error) {
ID: order.ID, TradeNo: order.TradeNo, Provider: order.Provider,
ProviderTradeNo: optionalString(order.ProviderTradeNo), QueryID: order.QueryID,
BusinessType: order.BusinessType, BusinessID: order.BusinessID, Subject: order.Subject,
PaymentMode: defaultString(order.PaymentMode, biz.PaymentModeExternal), OriginalAmount: order.OriginalAmount,
PaymentMode: defaultString(order.PaymentMode, system.PaymentModeExternal), OriginalAmount: order.OriginalAmount,
Amount: order.Amount, PaidAmount: order.PaidAmount, PayerPaidAmount: order.PayerPaidAmount,
CashPaidAmount: order.CashPaidAmount, PointPaidAmount: order.PointPaidAmount, DiscountAmount: order.DiscountAmount,
ProviderDiscountAmount: order.ProviderDiscountAmount, MerchantDiscountAmount: order.MerchantDiscountAmount,
SettlementAmount: order.SettlementAmount, Currency: order.Currency, PayerCurrency: order.PayerCurrency,
AmountBreakdownKnown: order.AmountBreakdownKnown,
PaymentStatus: defaultString(order.PaymentStatus, biz.PaymentStatusInitialized),
PaymentStatus: defaultString(order.PaymentStatus, system.PaymentStatusInitialized),
ProviderStatus: order.ProviderStatus,
FulfillmentStatus: defaultString(order.FulfillmentStatus, biz.FulfillmentStatusPending),
RefundStatus: defaultString(order.RefundStatus, biz.RefundStatusNone),
FulfillmentStatus: defaultString(order.FulfillmentStatus, system.FulfillmentStatusPending),
RefundStatus: defaultString(order.RefundStatus, system.RefundStatusNone),
RefundedAmount: order.RefundedAmount, RefundRequestedAmount: order.RefundRequestedAmount, RefundNo: order.RefundNo,
ConfirmationID: order.ConfirmationID, RequestFingerprint: order.RequestFingerprint,
CreatePayload: createPayload, Extra: extra, LastEventID: order.LastEventID,
@ -104,15 +105,15 @@ func newPaymentOrderPO(order *biz.PaymentOrder) (*paymentOrderPO, error) {
}, nil
}
func toBizPaymentOrder(po *paymentOrderPO) *biz.PaymentOrder {
func toBizPaymentOrder(po *paymentOrderPO) *system.PaymentOrder {
if po == nil {
return nil
}
return &biz.PaymentOrder{
return &system.PaymentOrder{
ID: po.ID, TradeNo: po.TradeNo, Provider: po.Provider,
ProviderTradeNo: dereferenceString(po.ProviderTradeNo), QueryID: po.QueryID,
BusinessType: po.BusinessType, BusinessID: po.BusinessID, Subject: po.Subject,
PaymentMode: defaultString(po.PaymentMode, biz.PaymentModeExternal), OriginalAmount: po.OriginalAmount,
PaymentMode: defaultString(po.PaymentMode, system.PaymentModeExternal), OriginalAmount: po.OriginalAmount,
Amount: po.Amount, PaidAmount: po.PaidAmount, PayerPaidAmount: po.PayerPaidAmount,
CashPaidAmount: po.CashPaidAmount, PointPaidAmount: po.PointPaidAmount, DiscountAmount: po.DiscountAmount,
ProviderDiscountAmount: po.ProviderDiscountAmount, MerchantDiscountAmount: po.MerchantDiscountAmount,
@ -130,7 +131,7 @@ func toBizPaymentOrder(po *paymentOrderPO) *biz.PaymentOrder {
}
}
func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *biz.PaymentOrder) (*biz.PaymentOrder, bool, error) {
func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *system.PaymentOrder) (*system.PaymentOrder, bool, error) {
po, err := newPaymentOrderPO(order)
if err != nil {
return nil, false, err
@ -153,18 +154,18 @@ func (r *paymentOrderRepo) CreatePaymentOrder(ctx context.Context, order *biz.Pa
return nil, false, err
}
func (r *paymentOrderRepo) FindPaymentOrder(ctx context.Context, provider, tradeNo string) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) FindPaymentOrder(ctx context.Context, provider, tradeNo string) (*system.PaymentOrder, error) {
var po paymentOrderPO
if err := r.data.DB().WithContext(ctx).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrPaymentOrderNotFound
return nil, system.ErrPaymentOrderNotFound
}
return nil, err
}
return toBizPaymentOrder(&po), nil
}
func (r *paymentOrderRepo) ListPaymentOrders(ctx context.Context, page, pageSize int, filter biz.PaymentOrderFilter) ([]*biz.PaymentOrder, int64, error) {
func (r *paymentOrderRepo) ListPaymentOrders(ctx context.Context, page, pageSize int, filter system.PaymentOrderFilter) ([]*system.PaymentOrder, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&paymentOrderPO{})
if value := strings.TrimSpace(filter.Provider); value != "" {
db = db.Where("provider = ?", value)
@ -192,14 +193,14 @@ func (r *paymentOrderRepo) ListPaymentOrders(ctx context.Context, page, pageSize
if err := pagination.ApplyRequired(db.Order("id desc"), page, pageSize, 100).Find(&rows).Error; err != nil {
return nil, 0, err
}
items := make([]*biz.PaymentOrder, 0, len(rows))
items := make([]*system.PaymentOrder, 0, len(rows))
for i := range rows {
items = append(items, toBizPaymentOrder(&rows[i]))
}
return items, total, nil
}
func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tradeNo string, update *biz.PaymentProviderUpdate) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tradeNo string, update *system.PaymentProviderUpdate) (*system.PaymentOrder, error) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if update == nil {
return errors.New("支付下单结果为空")
@ -214,11 +215,11 @@ func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tr
po.LastEventID = trimTo(update.EventID, 128)
po.LastPayloadHash = trimTo(update.PayloadHash, 64)
status := normalizeOrderPaymentStatus(update.Status)
if status == biz.PaymentStatusPaid {
if status == system.PaymentStatusPaid {
// Provider create responses are never sufficient proof of payment.
status = biz.PaymentStatusPending
status = system.PaymentStatusPending
}
if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded && status != "" {
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded && status != "" {
po.PaymentStatus = status
}
po.Version++
@ -226,7 +227,7 @@ func (r *paymentOrderRepo) RecordPaymentCreate(ctx context.Context, provider, tr
})
}
func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tradeNo string, update *biz.PaymentProviderUpdate) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tradeNo string, update *system.PaymentProviderUpdate) (*system.PaymentOrder, error) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if update == nil {
return errors.New("支付查单结果为空")
@ -235,13 +236,13 @@ func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tra
return err
}
if update.Amount > 0 && update.Amount != po.Amount {
return biz.ErrPaymentOrderConflict
return system.ErrPaymentOrderConflict
}
if update.PayerPaidAmount < 0 || update.CashPaidAmount < 0 || update.PointPaidAmount < 0 || update.DiscountAmount < 0 || update.ProviderDiscountAmount < 0 || update.MerchantDiscountAmount < 0 || update.SettlementAmount < 0 {
return biz.ErrPaymentOrderConflict
return system.ErrPaymentOrderConflict
}
if update.Currency != "" && !strings.EqualFold(update.Currency, po.Currency) {
return biz.ErrPaymentOrderConflict
return system.ErrPaymentOrderConflict
}
po.ProviderStatus = trimTo(update.ProviderStatus, 64)
po.LastEventID = trimTo(update.EventID, 128)
@ -258,22 +259,22 @@ func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tra
po.AmountBreakdownKnown = true
}
switch normalizeOrderPaymentStatus(update.Status) {
case biz.PaymentStatusPaid:
if po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded {
po.PaymentStatus = biz.PaymentStatusPaid
case system.PaymentStatusPaid:
if po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded {
po.PaymentStatus = system.PaymentStatusPaid
}
po.PaidAmount = po.Amount
if po.PaidAt == nil {
now := time.Now().UTC()
po.PaidAt = &now
}
case biz.PaymentStatusPending:
if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded {
po.PaymentStatus = biz.PaymentStatusPending
case system.PaymentStatusPending:
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded {
po.PaymentStatus = system.PaymentStatusPending
}
case biz.PaymentStatusFailed:
if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded && po.PaymentStatus != biz.PaymentStatusRefunded {
po.PaymentStatus = biz.PaymentStatusFailed
case system.PaymentStatusFailed:
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded && po.PaymentStatus != system.PaymentStatusRefunded {
po.PaymentStatus = system.PaymentStatusFailed
}
}
po.Version++
@ -281,27 +282,27 @@ func (r *paymentOrderRepo) ApplyPaymentResult(ctx context.Context, provider, tra
})
}
func (r *paymentOrderRepo) BeginPaymentFulfillment(ctx context.Context, provider, tradeNo string, lease time.Duration) (*biz.PaymentOrder, string, bool, error) {
func (r *paymentOrderRepo) BeginPaymentFulfillment(ctx context.Context, provider, tradeNo string, lease time.Duration) (*system.PaymentOrder, string, bool, error) {
var token string
var duplicate bool
order, err := r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if po.FulfillmentStatus == biz.FulfillmentStatusSucceeded {
if po.FulfillmentStatus == system.FulfillmentStatusSucceeded {
duplicate = true
return nil
}
if po.PaymentStatus != biz.PaymentStatusPaid {
return biz.ErrPaymentOrderState
if po.PaymentStatus != system.PaymentStatusPaid {
return system.ErrPaymentOrderState
}
now := time.Now().UTC()
if po.FulfillmentStatus == biz.FulfillmentStatusProcessing && po.FulfillmentLeaseUntil != nil && po.FulfillmentLeaseUntil.After(now) {
return biz.ErrPaymentOrderBusy
if po.FulfillmentStatus == system.FulfillmentStatusProcessing && po.FulfillmentLeaseUntil != nil && po.FulfillmentLeaseUntil.After(now) {
return system.ErrPaymentOrderBusy
}
if lease <= 0 {
lease = 10 * time.Minute
}
token = uuid.NewString()
until := now.Add(lease)
po.FulfillmentStatus = biz.FulfillmentStatusProcessing
po.FulfillmentStatus = system.FulfillmentStatusProcessing
po.FulfillmentToken = token
po.FulfillmentLeaseUntil = &until
po.LastError = ""
@ -311,56 +312,56 @@ func (r *paymentOrderRepo) BeginPaymentFulfillment(ctx context.Context, provider
return order, token, duplicate, err
}
func (r *paymentOrderRepo) CompletePaymentFulfillment(ctx context.Context, provider, tradeNo, token string, success bool, message string) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) CompletePaymentFulfillment(ctx context.Context, provider, tradeNo, token string, success bool, message string) (*system.PaymentOrder, error) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if po.FulfillmentStatus != biz.FulfillmentStatusProcessing || po.FulfillmentToken != token {
return biz.ErrPaymentOrderBusy
if po.FulfillmentStatus != system.FulfillmentStatusProcessing || po.FulfillmentToken != token {
return system.ErrPaymentOrderBusy
}
po.FulfillmentToken = ""
po.FulfillmentLeaseUntil = nil
po.LastError = trimTo(message, 512)
if success {
po.FulfillmentStatus = biz.FulfillmentStatusSucceeded
po.FulfillmentStatus = system.FulfillmentStatusSucceeded
now := time.Now().UTC()
po.FulfilledAt = &now
} else {
po.FulfillmentStatus = biz.FulfillmentStatusFailed
po.FulfillmentStatus = system.FulfillmentStatusFailed
}
po.Version++
return tx.Save(po).Error
})
}
func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tradeNo string, amount int64, lease time.Duration) (*biz.PaymentOrder, string, error) {
func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tradeNo string, amount int64, lease time.Duration) (*system.PaymentOrder, string, error) {
var token string
order, err := r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if po.PaymentStatus != biz.PaymentStatusPaid && po.PaymentStatus != biz.PaymentStatusPartiallyRefunded {
return biz.ErrPaymentOrderState
if po.PaymentStatus != system.PaymentStatusPaid && po.PaymentStatus != system.PaymentStatusPartiallyRefunded {
return system.ErrPaymentOrderState
}
if amount <= 0 {
return biz.ErrPaymentOrderConflict
return system.ErrPaymentOrderConflict
}
now := time.Now().UTC()
if po.RefundStatus == biz.RefundStatusProcessing && po.RefundLeaseUntil != nil && po.RefundLeaseUntil.After(now) {
return biz.ErrPaymentOrderBusy
if po.RefundStatus == system.RefundStatusProcessing && po.RefundLeaseUntil != nil && po.RefundLeaseUntil.After(now) {
return system.ErrPaymentOrderBusy
}
if po.RefundStatus == biz.RefundStatusProcessing && po.RefundRequestedAmount != amount {
return biz.ErrPaymentOrderConflict
if po.RefundStatus == system.RefundStatusProcessing && po.RefundRequestedAmount != amount {
return system.ErrPaymentOrderConflict
}
if po.RefundStatus == biz.RefundStatusPending {
return biz.ErrPaymentOrderBusy
if po.RefundStatus == system.RefundStatusPending {
return system.ErrPaymentOrderBusy
}
// An expired processing lease means the provider outcome is unknown.
// Retry the same refund amount with the same durable refund number. A
// different amount must never reuse that operation identity.
reserved := int64(0)
if po.RefundStatus == biz.RefundStatusProcessing && po.RefundLeaseUntil != nil && !po.RefundLeaseUntil.After(now) {
if po.RefundStatus == system.RefundStatusProcessing && po.RefundLeaseUntil != nil && !po.RefundLeaseUntil.After(now) {
reserved = 0
} else {
reserved = po.RefundRequestedAmount
}
if amount > po.Amount-po.RefundedAmount-reserved {
return biz.ErrPaymentOrderConflict
return system.ErrPaymentOrderConflict
}
if lease <= 0 {
lease = 10 * time.Minute
@ -370,7 +371,7 @@ func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tra
po.RefundNo = uuid.NewString()
}
until := now.Add(lease)
po.RefundStatus = biz.RefundStatusProcessing
po.RefundStatus = system.RefundStatusProcessing
po.RefundRequestedAmount = amount
po.RefundToken = token
po.RefundLeaseUntil = &until
@ -381,18 +382,18 @@ func (r *paymentOrderRepo) BeginPaymentRefund(ctx context.Context, provider, tra
return order, token, err
}
func (r *paymentOrderRepo) CompletePaymentRefundRequest(ctx context.Context, provider, tradeNo, token string, accepted bool, message string) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) CompletePaymentRefundRequest(ctx context.Context, provider, tradeNo, token string, accepted bool, message string) (*system.PaymentOrder, error) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if po.RefundStatus != biz.RefundStatusProcessing || po.RefundToken != token {
return biz.ErrPaymentOrderBusy
if po.RefundStatus != system.RefundStatusProcessing || po.RefundToken != token {
return system.ErrPaymentOrderBusy
}
po.RefundToken = ""
po.RefundLeaseUntil = nil
po.LastError = trimTo(message, 512)
if accepted {
po.RefundStatus = biz.RefundStatusPending
po.RefundStatus = system.RefundStatusPending
} else {
po.RefundStatus = biz.RefundStatusFailed
po.RefundStatus = system.RefundStatusFailed
po.RefundRequestedAmount = 0
po.RefundNo = ""
}
@ -401,26 +402,26 @@ func (r *paymentOrderRepo) CompletePaymentRefundRequest(ctx context.Context, pro
})
}
func (r *paymentOrderRepo) ConfirmPaymentRefund(ctx context.Context, provider, tradeNo, refundNo string, amount int64, success bool, message string) (*biz.PaymentOrder, error) {
func (r *paymentOrderRepo) ConfirmPaymentRefund(ctx context.Context, provider, tradeNo, refundNo string, amount int64, success bool, message string) (*system.PaymentOrder, error) {
return r.withLockedOrder(ctx, provider, tradeNo, func(tx *gorm.DB, po *paymentOrderPO) error {
if po.RefundStatus != biz.RefundStatusPending || po.RefundNo == "" || po.RefundNo != refundNo || po.RefundRequestedAmount != amount {
return biz.ErrPaymentOrderState
if po.RefundStatus != system.RefundStatusPending || po.RefundNo == "" || po.RefundNo != refundNo || po.RefundRequestedAmount != amount {
return system.ErrPaymentOrderState
}
po.LastError = trimTo(message, 512)
po.RefundRequestedAmount = 0
if !success {
po.RefundStatus = biz.RefundStatusFailed
po.RefundStatus = system.RefundStatusFailed
po.RefundNo = ""
po.Version++
return tx.Save(po).Error
}
po.RefundedAmount += amount
if po.RefundedAmount >= po.Amount {
po.PaymentStatus = biz.PaymentStatusRefunded
po.RefundStatus = biz.RefundStatusSucceeded
po.PaymentStatus = system.PaymentStatusRefunded
po.RefundStatus = system.RefundStatusSucceeded
} else {
po.PaymentStatus = biz.PaymentStatusPartiallyRefunded
po.RefundStatus = biz.RefundStatusPartial
po.PaymentStatus = system.PaymentStatusPartiallyRefunded
po.RefundStatus = system.RefundStatusPartial
}
po.RefundNo = ""
now := time.Now().UTC()
@ -430,13 +431,13 @@ func (r *paymentOrderRepo) ConfirmPaymentRefund(ctx context.Context, provider, t
})
}
func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeNo string, fn func(*gorm.DB, *paymentOrderPO) error) (*biz.PaymentOrder, error) {
var result *biz.PaymentOrder
func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeNo string, fn func(*gorm.DB, *paymentOrderPO) error) (*system.PaymentOrder, error) {
var result *system.PaymentOrder
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var po paymentOrderPO
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("provider = ? AND trade_no = ?", provider, tradeNo).First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return biz.ErrPaymentOrderNotFound
return system.ErrPaymentOrderNotFound
}
return err
}
@ -449,16 +450,16 @@ func (r *paymentOrderRepo) withLockedOrder(ctx context.Context, provider, tradeN
return result, err
}
func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *biz.PaymentProviderUpdate) error {
func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *system.PaymentProviderUpdate) error {
if update.ProviderTradeNo != "" {
if po.ProviderTradeNo != nil && *po.ProviderTradeNo != update.ProviderTradeNo {
return biz.ErrPaymentProviderConflict
return system.ErrPaymentProviderConflict
}
var other paymentOrderPO
// This check is repeated under the order transaction so a platform
// transaction cannot be attached to a different merchant order.
if err := tx.Where("provider = ? AND provider_trade_no = ? AND id <> ?", po.Provider, update.ProviderTradeNo, po.ID).First(&other).Error; err == nil {
return biz.ErrPaymentProviderConflict
return system.ErrPaymentProviderConflict
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
@ -467,7 +468,7 @@ func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *biz.PaymentP
}
if update.QueryID != "" {
if po.QueryID != "" && po.QueryID != update.QueryID {
return biz.ErrPaymentProviderConflict
return system.ErrPaymentProviderConflict
}
po.QueryID = update.QueryID
}
@ -477,11 +478,11 @@ func applyProviderIdentity(tx *gorm.DB, po *paymentOrderPO, update *biz.PaymentP
func normalizeOrderPaymentStatus(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "success", "paid", "fulfilled":
return biz.PaymentStatusPaid
return system.PaymentStatusPaid
case "pending", "created", "client_pending", "processing":
return biz.PaymentStatusPending
return system.PaymentStatusPending
case "failed", "closed", "cancelled", "canceled":
return biz.PaymentStatusFailed
return system.PaymentStatusFailed
default:
return ""
}

View File

@ -2,10 +2,9 @@ package payment
import (
"context"
"kra/internal/biz/system"
"testing"
"time"
"kra/internal/biz"
)
func newPaymentOrderRepoForTest(t *testing.T) *paymentOrderRepo {
@ -20,11 +19,11 @@ func newPaymentOrderRepoForTest(t *testing.T) *paymentOrderRepo {
return &paymentOrderRepo{data: &Data{gormDB: newReloadableDB(db, nil)}}
}
func testPaymentOrder() *biz.PaymentOrder {
return &biz.PaymentOrder{
TradeNo: "order-1", Provider: biz.PaymentAlipay, BusinessType: "game_item", BusinessID: "item-1",
Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: biz.PaymentStatusInitialized,
FulfillmentStatus: biz.FulfillmentStatusPending, RefundStatus: biz.RefundStatusNone,
func testPaymentOrder() *system.PaymentOrder {
return &system.PaymentOrder{
TradeNo: "order-1", Provider: system.PaymentAlipay, BusinessType: "game_item", BusinessID: "item-1",
Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: system.PaymentStatusInitialized,
FulfillmentStatus: system.FulfillmentStatusPending, RefundStatus: system.RefundStatusNone,
ConfirmationID: "11111111-1111-1111-1111-111111111111", RequestFingerprint: "fingerprint",
}
}
@ -36,36 +35,36 @@ func TestPaymentOrderRepositoryPersistsPaymentFulfillmentAndRefundState(t *testi
if err != nil || !created {
t.Fatalf("create order = %#v created=%v err=%v", order, created, err)
}
update := &biz.PaymentProviderUpdate{Status: "success", ProviderStatus: "TRADE_SUCCESS", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY", EventID: "event-1"}
order, err = repo.ApplyPaymentResult(ctx, biz.PaymentAlipay, "order-1", update)
if err != nil || order.PaymentStatus != biz.PaymentStatusPaid || order.PaidAmount != 100 {
update := &system.PaymentProviderUpdate{Status: "success", ProviderStatus: "TRADE_SUCCESS", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY", EventID: "event-1"}
order, err = repo.ApplyPaymentResult(ctx, system.PaymentAlipay, "order-1", update)
if err != nil || order.PaymentStatus != system.PaymentStatusPaid || order.PaidAmount != 100 {
t.Fatalf("apply payment = %#v err=%v", order, err)
}
order, token, duplicate, err := repo.BeginPaymentFulfillment(ctx, biz.PaymentAlipay, "order-1", time.Minute)
if err != nil || duplicate || token == "" || order.FulfillmentStatus != biz.FulfillmentStatusProcessing {
order, token, duplicate, err := repo.BeginPaymentFulfillment(ctx, system.PaymentAlipay, "order-1", time.Minute)
if err != nil || duplicate || token == "" || order.FulfillmentStatus != system.FulfillmentStatusProcessing {
t.Fatalf("begin fulfillment = %#v token=%q duplicate=%v err=%v", order, token, duplicate, err)
}
if _, _, _, err = repo.BeginPaymentFulfillment(ctx, biz.PaymentAlipay, "order-1", time.Minute); err == nil {
if _, _, _, err = repo.BeginPaymentFulfillment(ctx, system.PaymentAlipay, "order-1", time.Minute); err == nil {
t.Fatal("concurrent fulfillment was accepted")
}
order, err = repo.CompletePaymentFulfillment(ctx, biz.PaymentAlipay, "order-1", token, true, "")
if err != nil || order.FulfillmentStatus != biz.FulfillmentStatusSucceeded {
order, err = repo.CompletePaymentFulfillment(ctx, system.PaymentAlipay, "order-1", token, true, "")
if err != nil || order.FulfillmentStatus != system.FulfillmentStatusSucceeded {
t.Fatalf("complete fulfillment = %#v err=%v", order, err)
}
_, _, duplicate, err = repo.BeginPaymentFulfillment(ctx, biz.PaymentAlipay, "order-1", time.Minute)
_, _, duplicate, err = repo.BeginPaymentFulfillment(ctx, system.PaymentAlipay, "order-1", time.Minute)
if err != nil || !duplicate {
t.Fatalf("duplicate fulfillment = duplicate=%v err=%v", duplicate, err)
}
order, token, err = repo.BeginPaymentRefund(ctx, biz.PaymentAlipay, "order-1", 40, time.Minute)
if err != nil || token == "" || order.RefundStatus != biz.RefundStatusProcessing {
order, token, err = repo.BeginPaymentRefund(ctx, system.PaymentAlipay, "order-1", 40, time.Minute)
if err != nil || token == "" || order.RefundStatus != system.RefundStatusProcessing {
t.Fatalf("begin refund = %#v token=%q err=%v", order, token, err)
}
order, err = repo.CompletePaymentRefundRequest(ctx, biz.PaymentAlipay, "order-1", token, true, "")
if err != nil || order.RefundStatus != biz.RefundStatusPending || order.RefundRequestedAmount != 40 {
order, err = repo.CompletePaymentRefundRequest(ctx, system.PaymentAlipay, "order-1", token, true, "")
if err != nil || order.RefundStatus != system.RefundStatusPending || order.RefundRequestedAmount != 40 {
t.Fatalf("accept refund = %#v err=%v", order, err)
}
order, err = repo.ConfirmPaymentRefund(ctx, biz.PaymentAlipay, "order-1", order.RefundNo, 40, true, "")
if err != nil || order.RefundedAmount != 40 || order.PaymentStatus != biz.PaymentStatusPartiallyRefunded {
order, err = repo.ConfirmPaymentRefund(ctx, system.PaymentAlipay, "order-1", order.RefundNo, 40, true, "")
if err != nil || order.RefundedAmount != 40 || order.PaymentStatus != system.PaymentStatusPartiallyRefunded {
t.Fatalf("confirm refund = %#v err=%v", order, err)
}
}
@ -83,11 +82,11 @@ func TestPaymentOrderRepositoryRejectsProviderTradeReuse(t *testing.T) {
if _, _, err := repo.CreatePaymentOrder(ctx, second); err != nil {
t.Fatal(err)
}
update := &biz.PaymentProviderUpdate{Status: "success", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY"}
if _, err := repo.ApplyPaymentResult(ctx, biz.PaymentAlipay, "order-1", update); err != nil {
update := &system.PaymentProviderUpdate{Status: "success", ProviderTradeNo: "provider-1", Amount: 100, Currency: "CNY"}
if _, err := repo.ApplyPaymentResult(ctx, system.PaymentAlipay, "order-1", update); err != nil {
t.Fatal(err)
}
if _, err := repo.ApplyPaymentResult(ctx, biz.PaymentAlipay, "order-2", update); err != biz.ErrPaymentProviderConflict {
if _, err := repo.ApplyPaymentResult(ctx, system.PaymentAlipay, "order-2", update); err != system.ErrPaymentProviderConflict {
t.Fatalf("provider trade reuse err = %v", err)
}
}
@ -101,13 +100,13 @@ func TestPaymentOrderRepositoryListsWithFilters(t *testing.T) {
}
second := testPaymentOrder()
second.TradeNo = "wechat-order-2"
second.Provider = biz.PaymentWechatV3
second.Provider = system.PaymentWechatV3
second.BusinessID = "item-2"
second.ConfirmationID = "33333333-3333-3333-3333-333333333333"
if _, _, err := repo.CreatePaymentOrder(ctx, second); err != nil {
t.Fatal(err)
}
items, total, err := repo.ListPaymentOrders(ctx, 1, 10, biz.PaymentOrderFilter{Provider: biz.PaymentWechatV3, TradeNo: "wechat", BusinessID: "item-2"})
items, total, err := repo.ListPaymentOrders(ctx, 1, 10, system.PaymentOrderFilter{Provider: system.PaymentWechatV3, TradeNo: "wechat", BusinessID: "item-2"})
if err != nil {
t.Fatal(err)
}

View File

@ -3,9 +3,9 @@ package system
import (
"context"
"encoding/json"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/gormkit"
"kra/pkg/database/pagination"
@ -27,17 +27,17 @@ func (announcementPO) TableName() string { return "sys_announcements" }
type announcementRepo struct{ data Provider }
func NewAnnouncementRepo(data Provider) biz.AnnouncementRepo { return &announcementRepo{data: data} }
func NewAnnouncementRepo(data Provider) system.AnnouncementRepo { return &announcementRepo{data: data} }
func newAnnouncement(item *biz.Announcement) announcementPO {
func newAnnouncement(item *system.Announcement) announcementPO {
return announcementPO{ID: item.ID, Title: item.Title, Content: item.Content, UserID: item.UserID, Attachments: gormkit.JSON(item.Attachments)}
}
func announcementToBiz(item announcementPO) *biz.Announcement {
return &biz.Announcement{ID: item.ID, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, Title: item.Title, Content: item.Content, UserID: item.UserID, Attachments: json.RawMessage(item.Attachments)}
func announcementToBiz(item announcementPO) *system.Announcement {
return &system.Announcement{ID: item.ID, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, Title: item.Title, Content: item.Content, UserID: item.UserID, Attachments: json.RawMessage(item.Attachments)}
}
func (r *announcementRepo) Create(ctx context.Context, item *biz.Announcement) error {
func (r *announcementRepo) Create(ctx context.Context, item *system.Announcement) error {
po := newAnnouncement(item)
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@ -54,12 +54,12 @@ func (r *announcementRepo) DeleteByIDs(ctx context.Context, ids []string) error
return r.data.DB().WithContext(ctx).Delete(&[]announcementPO{}, "id IN ?", ids).Error
}
func (r *announcementRepo) Update(ctx context.Context, item *biz.Announcement) error {
func (r *announcementRepo) Update(ctx context.Context, item *system.Announcement) error {
po := newAnnouncement(item)
return r.data.DB().WithContext(ctx).Model(&announcementPO{}).Where("id = ?", item.ID).Updates(&po).Error
}
func (r *announcementRepo) Find(ctx context.Context, id string) (*biz.Announcement, error) {
func (r *announcementRepo) Find(ctx context.Context, id string) (*system.Announcement, error) {
var po announcementPO
if err := r.data.DB().WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil {
return nil, err
@ -67,7 +67,7 @@ func (r *announcementRepo) Find(ctx context.Context, id string) (*biz.Announceme
return announcementToBiz(po), nil
}
func (r *announcementRepo) List(ctx context.Context, filter biz.AnnouncementFilter) ([]*biz.Announcement, int64, error) {
func (r *announcementRepo) List(ctx context.Context, filter system.AnnouncementFilter) ([]*system.Announcement, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&announcementPO{})
if filter.StartCreatedAt != nil && filter.EndCreatedAt != nil {
db = db.Where("created_at BETWEEN ? AND ?", filter.StartCreatedAt, filter.EndCreatedAt)
@ -83,14 +83,14 @@ func (r *announcementRepo) List(ctx context.Context, filter biz.AnnouncementFilt
if err := db.Find(&pos).Error; err != nil {
return nil, 0, err
}
items := make([]*biz.Announcement, 0, len(pos))
items := make([]*system.Announcement, 0, len(pos))
for _, po := range pos {
items = append(items, announcementToBiz(po))
}
return items, total, nil
}
func (r *announcementRepo) UserOptions(ctx context.Context) ([]biz.UserOption, error) {
func (r *announcementRepo) UserOptions(ctx context.Context) ([]system.UserOption, error) {
var rows []struct {
Label string
Value uint
@ -98,9 +98,9 @@ func (r *announcementRepo) UserOptions(ctx context.Context) ([]biz.UserOption, e
// The generated data-source endpoint is best effort: return collected
// options even when the underlying scan reports an error.
_ = r.data.DB().WithContext(ctx).Table("sys_users").Select("nick_name AS label, id AS value").Scan(&rows).Error
items := make([]biz.UserOption, 0, len(rows))
items := make([]system.UserOption, 0, len(rows))
for _, row := range rows {
items = append(items, biz.UserOption{Label: row.Label, Value: row.Value})
items = append(items, system.UserOption{Label: row.Label, Value: row.Value})
}
return items, nil
}

View File

@ -2,16 +2,15 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
)
func TestAnnouncementRepositoryKeepsRawIDQuerySemantics(t *testing.T) {
data := newTransactionTestData(t)
repo := &announcementRepo{data: data}
ctx := context.Background()
item := &biz.Announcement{Title: "notice"}
item := &system.Announcement{Title: "notice"}
if err := repo.Create(ctx, item); err != nil {
t.Fatal(err)
}
@ -44,7 +43,7 @@ func TestAnnouncementRepositoryPreservesSignedUserID(t *testing.T) {
repo := &announcementRepo{data: data}
ctx := context.Background()
userID := -1
item := &biz.Announcement{Title: "notice", UserID: &userID}
item := &system.Announcement{Title: "notice", UserID: &userID}
if err := repo.Create(ctx, item); err != nil {
t.Fatal(err)
}

View File

@ -3,10 +3,10 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"strconv"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@ -14,7 +14,7 @@ import (
type apiRepo struct{ data Provider }
func NewAPIRepo(data Provider) biz.APIRepo { return &apiRepo{data: data} }
func NewAPIRepo(data Provider) system.APIRepo { return &apiRepo{data: data} }
type apiPO struct {
ID uint `gorm:"primaryKey"`
@ -47,10 +47,10 @@ type authorityAPIPO struct {
func (authorityAPIPO) TableName() string { return "sys_authority_apis" }
func apiFromPO(po apiPO) *biz.API {
return &biz.API{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Path: po.Path, Description: po.Description, APIGroup: po.APIGroup, Method: po.Method}
func apiFromPO(po apiPO) *system.API {
return &system.API{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Path: po.Path, Description: po.Description, APIGroup: po.APIGroup, Method: po.Method}
}
func (r *apiRepo) CreateAPI(ctx context.Context, v *biz.API) error {
func (r *apiRepo) CreateAPI(ctx context.Context, v *system.API) error {
po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: v.Method}
var count int64
if err := r.data.DB().WithContext(ctx).Model(&apiPO{}).Where("path = ? AND method = ?", po.Path, po.Method).Count(&count).Error; err != nil {
@ -65,7 +65,7 @@ func (r *apiRepo) CreateAPI(ctx context.Context, v *biz.API) error {
v.ID, v.CreatedAt, v.UpdatedAt, v.Method = po.ID, po.CreatedAt, po.UpdatedAt, po.Method
return nil
}
func (r *apiRepo) UpdateAPI(ctx context.Context, v *biz.API) error {
func (r *apiRepo) UpdateAPI(ctx context.Context, v *system.API) error {
db := r.data.DB().WithContext(ctx)
var old apiPO
if err := db.First(&old, v.ID).Error; err != nil {
@ -109,18 +109,18 @@ func (r *apiRepo) DeleteAPIs(ctx context.Context, ids []uint) error {
return tx.Delete(&apiPO{}, ids).Error
})
}
func (r *apiRepo) FindAPI(ctx context.Context, id uint) (*biz.API, error) {
func (r *apiRepo) FindAPI(ctx context.Context, id uint) (*system.API, error) {
var po apiPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return apiFromPO(po), nil
}
func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*biz.API, int64, error) {
func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *system.API) ([]*system.API, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&apiPO{})
if q != nil && q.StrictAll {
config := r.data.Runtime().Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
if actor, ok := system.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var authority authorityPO
if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return nil, 0, err
@ -173,7 +173,7 @@ func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*
if err := query.Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.API, 0, len(pos))
out := make([]*system.API, 0, len(pos))
for _, po := range pos {
out = append(out, apiFromPO(po))
}

View File

@ -3,10 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"strconv"
"kra/internal/biz"
"github.com/casbin/casbin/v3"
casbinmodel "github.com/casbin/casbin/v3/model"
"gorm.io/gorm"
@ -43,7 +42,7 @@ func (r *apiRepo) SetAPIRoles(ctx context.Context, path, method string, ids []ui
return errors.New("您提交的角色ID不合法")
}
}
if err := r.checkPolicyPathsAuth(ctx, []*biz.API{{Path: path, Method: method}}); err != nil {
if err := r.checkPolicyPathsAuth(ctx, []*system.API{{Path: path, Method: method}}); err != nil {
return err
}
}
@ -105,21 +104,21 @@ m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && r.act == p.act`)
}
return enforcer.Enforce(subject, path, method)
}
func (r *apiRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) {
func (r *apiRepo) PolicyPaths(ctx context.Context, aid uint) ([]*system.API, error) {
rows, err := policyRowsForAuthority(r.data.DB().WithContext(ctx), aid)
if err != nil {
return nil, err
}
var out []*biz.API
var out []*system.API
if len(rows) > 0 {
out = make([]*biz.API, 0, len(rows))
out = make([]*system.API, 0, len(rows))
}
for _, row := range rows {
out = append(out, &biz.API{Path: row.V1, Method: row.V2})
out = append(out, &system.API{Path: row.V1, Method: row.V2})
}
return out, nil
}
func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API) error {
func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*system.API) error {
if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, aid); err != nil {
return err
}
@ -155,7 +154,7 @@ func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API
return nil
}
func (r *apiRepo) checkPolicyPathsAuth(ctx context.Context, paths []*biz.API) error {
func (r *apiRepo) checkPolicyPathsAuth(ctx context.Context, paths []*system.API) error {
actor, _, strict, err := (&authorityAccessRepo{data: r.data}).strictAuthorityAccess(ctx)
if err != nil || !strict {
return err

View File

@ -2,10 +2,10 @@ package system
import (
"context"
"kra/internal/biz/system"
"strings"
"testing"
"kra/internal/biz"
"kra/internal/conf"
)
@ -33,7 +33,7 @@ func apiPolicyUintPointer(value uint) *uint { return &value }
func TestSetPolicyPathsStrictRootRequiresRegisteredAPI(t *testing.T) {
data := newPolicyTestData(t)
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: 888})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: 888})
if err := data.gormDB.WithContext(ctx).Create(&[]authorityPO{
{AuthorityID: 888, ParentID: apiPolicyUintPointer(0)},
{AuthorityID: 999, ParentID: apiPolicyUintPointer(888)},
@ -44,18 +44,18 @@ func TestSetPolicyPathsStrictRootRequiresRegisteredAPI(t *testing.T) {
t.Fatal(err)
}
err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*biz.API{{Path: "/unknown", Method: "POST"}})
err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*system.API{{Path: "/unknown", Method: "POST"}})
if err == nil || !strings.Contains(err.Error(), "存在api不在权限列表中") {
t.Fatalf("unregistered root assignment error = %v", err)
}
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*biz.API{{Path: "/known", Method: "POST"}}); err != nil {
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*system.API{{Path: "/known", Method: "POST"}}); err != nil {
t.Fatal(err)
}
}
func TestSetPolicyPathsStrictChildRejectsStalePolicy(t *testing.T) {
data := newPolicyTestData(t)
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: 1001})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: 1001})
if err := data.gormDB.WithContext(ctx).Create(&[]authorityPO{
{AuthorityID: 888, ParentID: apiPolicyUintPointer(0)},
{AuthorityID: 1001, ParentID: apiPolicyUintPointer(888)},
@ -73,18 +73,18 @@ func TestSetPolicyPathsStrictChildRejectsStalePolicy(t *testing.T) {
t.Fatal(err)
}
err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 1002, []*biz.API{{Path: "/stale", Method: "POST"}})
err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 1002, []*system.API{{Path: "/stale", Method: "POST"}})
if err == nil || !strings.Contains(err.Error(), "存在api不在权限列表中") {
t.Fatalf("stale child assignment error = %v", err)
}
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 1002, []*biz.API{{Path: "/known", Method: "POST"}}); err != nil {
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 1002, []*system.API{{Path: "/known", Method: "POST"}}); err != nil {
t.Fatal(err)
}
}
func TestSetPolicyPathsUsesCompatibleDedupeKey(t *testing.T) {
data := newPolicyTestData(t)
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: 888})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: 888})
if err := data.gormDB.WithContext(ctx).Create(&[]authorityPO{
{AuthorityID: 888, ParentID: apiPolicyUintPointer(0)},
{AuthorityID: 999, ParentID: apiPolicyUintPointer(888)},
@ -98,7 +98,7 @@ func TestSetPolicyPathsUsesCompatibleDedupeKey(t *testing.T) {
t.Fatal(err)
}
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*biz.API{
if err := (&apiRepo{data: data}).SetPolicyPaths(ctx, 999, []*system.API{
{Path: "/a", Method: "BC"},
{Path: "/aB", Method: "C"},
}); err != nil {
@ -148,7 +148,7 @@ func TestSetAPIRolesStrictOnlyChangesManagedAuthorities(t *testing.T) {
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &apiRepo{data: data}
if err := repo.SetAPIRoles(ctx, "/known", "POST", []uint{siblingID}); err == nil {
t.Fatal("SetAPIRoles() accepted an out-of-scope authority")

View File

@ -2,20 +2,19 @@ package system
import (
"context"
"kra/internal/biz"
"kra/internal/biz/system"
"gorm.io/gorm"
)
func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) {
func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*system.API, error) {
var pos []ignoredAPIPO
if err := r.data.DB().WithContext(ctx).Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.API, 0, len(pos))
out := make([]*system.API, 0, len(pos))
for _, po := range pos {
out = append(out, &biz.API{Path: po.Path, Method: po.Method})
out = append(out, &system.API{Path: po.Path, Method: po.Method})
}
return out, nil
}
@ -28,7 +27,7 @@ func (r *apiRepo) SetAPIIgnored(ctx context.Context, path, method string, ignore
}
return r.data.DB().WithContext(ctx).Unscoped().Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error
}
func (r *apiRepo) ApplyAPISync(ctx context.Context, added, deleted []*biz.API) error {
func (r *apiRepo) ApplyAPISync(ctx context.Context, added, deleted []*system.API) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if len(added) > 0 {
pos := make([]apiPO, 0, len(added))

View File

@ -3,9 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@ -13,7 +13,7 @@ import (
type apiTokenRepo struct{ data Provider }
func NewAPITokenRepo(data Provider) biz.APITokenRepo { return &apiTokenRepo{data: data} }
func NewAPITokenRepo(data Provider) system.APITokenRepo { return &apiTokenRepo{data: data} }
type apiTokenPO struct {
ID uint `gorm:"primaryKey"`
@ -40,7 +40,7 @@ type jwtBlacklistPO struct {
func (jwtBlacklistPO) TableName() string { return "jwt_blacklists" }
func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*biz.User, bool, error) {
func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*system.User, bool, error) {
var po userPO
if err := r.data.DB().WithContext(ctx).First(&po, userID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@ -59,7 +59,7 @@ func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID
user, err := (&userRepo{data: r.data}).loadUser(ctx, &po)
return user, count > 0 || po.AuthorityID == authorityID, err
}
func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) error {
func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *system.APIToken) error {
po := apiTokenPO{UserID: v.UserID, AuthorityID: v.AuthorityID, Token: v.Token, Status: v.Status, ExpiresAt: v.ExpiresAt, Remark: v.Remark}
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@ -68,7 +68,7 @@ func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) erro
v.CreatedAt = po.CreatedAt
return nil
}
func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]*biz.APIToken, int64, error) {
func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]*system.APIToken, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&apiTokenPO{})
if userID != 0 {
db = db.Where("user_id = ?", userID)
@ -94,13 +94,13 @@ func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID
return nil, 0, err
}
}
users := make(map[uint]*biz.User, len(userPOs))
users := make(map[uint]*system.User, len(userPOs))
for i := range userPOs {
users[userPOs[i].ID] = baseBizUser(&userPOs[i])
}
out := make([]*biz.APIToken, 0, len(pos))
out := make([]*system.APIToken, 0, len(pos))
for _, po := range pos {
v := &biz.APIToken{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UserID: po.UserID, AuthorityID: po.AuthorityID, Token: po.Token, Status: po.Status, ExpiresAt: po.ExpiresAt, Remark: po.Remark, User: users[po.UserID]}
v := &system.APIToken{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UserID: po.UserID, AuthorityID: po.AuthorityID, Token: po.Token, Status: po.Status, ExpiresAt: po.ExpiresAt, Remark: po.Remark, User: users[po.UserID]}
out = append(out, v)
}
return out, total, nil

View File

@ -1,13 +1,17 @@
package system
import "kra/internal/biz"
import (
"kra/internal/biz/system"
)
type auditQueryRepo struct{ data Provider }
type auditRecorderRepo struct{ data Provider }
type logFileRepo struct{ data Provider }
func NewAuditRepo(data Provider) biz.AuditQueryRepo { return &auditQueryRepo{data: data} }
func NewAuditRepo(data Provider) system.AuditQueryRepo { return &auditQueryRepo{data: data} }
func NewAuditRecorderRepo(data Provider) biz.AuditRecordRepo { return &auditRecorderRepo{data: data} }
func NewAuditRecorderRepo(data Provider) system.AuditRecordRepo {
return &auditRecorderRepo{data: data}
}
func NewLogFileRepo(data Provider) biz.LogFileRepo { return &logFileRepo{data: data} }
func NewLogFileRepo(data Provider) system.LogFileRepo { return &logFileRepo{data: data} }

View File

@ -3,8 +3,7 @@ package system
import (
"context"
"errors"
"kra/internal/biz"
"kra/internal/biz/system"
"gorm.io/gorm"
)
@ -13,7 +12,7 @@ var errInvalidDataScope = errors.New("数据权限范围不合法")
type authorityAccessRepo struct{ data Provider }
func NewAuthorityAccessRepo(data Provider) biz.AuthorityAccessRepo {
func NewAuthorityAccessRepo(data Provider) system.AuthorityAccessRepo {
return &authorityAccessRepo{data: data}
}
@ -58,18 +57,18 @@ func (r *authorityAccessRepo) strictAuthorityIDs(ctx context.Context, actorID ui
return allowed, nil
}
func (r *authorityAccessRepo) strictAuthorityAccess(ctx context.Context) (biz.Actor, map[uint]bool, bool, error) {
func (r *authorityAccessRepo) strictAuthorityAccess(ctx context.Context) (system.Actor, map[uint]bool, bool, error) {
config := r.data.Runtime().Admin()
if config == nil || config.System == nil || !config.System.UseStrictAuth {
return biz.Actor{}, nil, false, nil
return system.Actor{}, nil, false, nil
}
actor, ok := biz.ActorFromContext(ctx)
actor, ok := system.ActorFromContext(ctx)
if !ok {
return biz.Actor{}, nil, true, errors.New("您提交的角色ID不合法")
return system.Actor{}, nil, true, errors.New("您提交的角色ID不合法")
}
allowed, err := r.strictAuthorityIDs(ctx, actor.AuthorityID)
if err != nil {
return biz.Actor{}, nil, true, err
return system.Actor{}, nil, true, err
}
return actor, allowed, true, nil
}
@ -91,7 +90,7 @@ func (r *authorityAccessRepo) checkAuthorityIDAuth(ctx context.Context, targetID
return r.checkAuthorityIDsAuth(ctx, []uint{targetID})
}
func managedAuthorityParent(actor biz.Actor, allowed map[uint]bool, targetID uint, parentID *uint, creating bool) (*uint, error) {
func managedAuthorityParent(actor system.Actor, allowed map[uint]bool, targetID uint, parentID *uint, creating bool) (*uint, error) {
if creating && (parentID == nil || *parentID == 0) {
value := actor.AuthorityID
return &value, nil
@ -131,7 +130,7 @@ func (r *authorityAccessRepo) ensureAuthorityParentAcyclic(ctx context.Context,
return nil
}
func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Authority) error {
func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *system.Authority) error {
if value.DataScope == 0 {
value.DataScope = 1
} else if value.DataScope < 1 || value.DataScope > 5 {
@ -168,7 +167,7 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
}
value.CreatedAt, value.UpdatedAt = po.CreatedAt, po.UpdatedAt
value.DataScope, value.DefaultRouter = po.DataScope, po.DefaultRouter
value.Menus = []*biz.Menu{{ID: 1, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Title: "仪表盘", Icon: "setting"}}
value.Menus = []*system.Menu{{ID: 1, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Title: "仪表盘", Icon: "setting"}}
var dashboard menuPO
if err := tx.Where("name = ?", "dashboard").First(&dashboard).Error; err == nil {
grantDashboard := true
@ -209,7 +208,7 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Au
return nil
})
}
func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *biz.Authority) error {
func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *system.Authority) error {
if value.DataScope == 0 {
value.DataScope = 1
} else if value.DataScope < 1 || value.DataScope > 5 {
@ -283,7 +282,7 @@ func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint,
if err := tx.Where("id IN ?", menuIDs).Order("sort").Find(&copiedMenus).Error; err != nil {
return err
}
value.Menus = make([]*biz.Menu, 0, len(copiedMenus))
value.Menus = make([]*system.Menu, 0, len(copiedMenus))
for _, menu := range copiedMenus {
value.Menus = append(value.Menus, menuFromPO(menu))
}
@ -362,7 +361,7 @@ func (r *authorityAccessRepo) copyPolicyAllowed(ctx context.Context, tx *gorm.DB
}
return policyExists(tx.WithContext(ctx), actorID, path, method)
}
func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error {
func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *system.Authority) error {
if err := r.checkAuthorityIDAuth(ctx, value.AuthorityID); err != nil {
return err
}
@ -464,11 +463,11 @@ func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) erro
return tx.Unscoped().Delete(&authorityPO{}, "authority_id = ?", id).Error
})
}
func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) {
func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*system.Authority, error) {
db := r.data.DB().WithContext(ctx)
var allowed map[uint]bool
config := r.data.Runtime().Admin()
if actor, ok := biz.ActorFromContext(ctx); ok {
if actor, ok := system.ActorFromContext(ctx); ok {
// The current authority is loaded even when strict mode is disabled;
// an invalid token authority therefore fails the list request instead of
// exposing the complete role tree.
@ -488,7 +487,7 @@ func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Autho
if err := db.Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.Authority, 0, len(pos))
out := make([]*system.Authority, 0, len(pos))
for _, po := range pos {
if allowed != nil && !allowed[po.AuthorityID] {
continue
@ -752,8 +751,8 @@ func (r *authorityAccessRepo) DataScopeDepartmentIDs(ctx context.Context, id uin
err := r.data.DB().WithContext(ctx).Model(&authorityDepartmentPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_department_id", &ids).Error
return ids, err
}
func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) {
identity := biz.DataScope{UserID: userID, AuthorityID: authorityID}
func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (system.DataScope, error) {
identity := system.DataScope{UserID: userID, AuthorityID: authorityID}
var user userPO
if err := r.data.DB().WithContext(ctx).Select("id", "dept_id").First(&user, userID).Error; err != nil {
return identity, err

View File

@ -3,9 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
"kra/internal/conf"
"gorm.io/gorm"
@ -44,8 +44,8 @@ func TestCopyAuthorityStrictPolicyValidationMatchesAdministrationContract(t *tes
}
repo := &authorityAccessRepo{data: data}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: rootID})
created := &biz.Authority{AuthorityID: 910, AuthorityName: "copy", ParentID: &rootID}
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: rootID})
created := &system.Authority{AuthorityID: 910, AuthorityName: "copy", ParentID: &rootID}
if err := repo.CopyAuthority(ctx, 900, created); err != nil {
t.Fatalf("root copy of a registered API failed: %v", err)
}
@ -53,7 +53,7 @@ func TestCopyAuthorityStrictPolicyValidationMatchesAdministrationContract(t *tes
t.Fatalf("copied policy exists = %v, err = %v", exists, err)
}
staleCopy := &biz.Authority{AuthorityID: 911, AuthorityName: "stale-copy", ParentID: &rootID}
staleCopy := &system.Authority{AuthorityID: 911, AuthorityName: "stale-copy", ParentID: &rootID}
if err := repo.CopyAuthority(ctx, 901, staleCopy); err == nil || err.Error() != "存在api不在权限列表中" {
t.Fatalf("stale API copy error = %v", err)
}
@ -78,8 +78,8 @@ func TestCopyAuthorityDuplicateIDWinsOverStrictParentValidation(t *testing.T) {
t.Fatal(err)
}
invalidParent := uint(999999)
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: rootID})
err := (&authorityAccessRepo{data: data}).CopyAuthority(ctx, 0, &biz.Authority{AuthorityID: 920, AuthorityName: "duplicate", ParentID: &invalidParent})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: rootID})
err := (&authorityAccessRepo{data: data}).CopyAuthority(ctx, 0, &system.Authority{AuthorityID: 920, AuthorityName: "duplicate", ParentID: &invalidParent})
if err == nil || err.Error() != "存在相同角色id" {
t.Fatalf("duplicate copy error = %v", err)
}
@ -87,7 +87,7 @@ func TestCopyAuthorityDuplicateIDWinsOverStrictParentValidation(t *testing.T) {
func TestListAuthoritiesRequiresCurrentAuthorityOutsideStrictMode(t *testing.T) {
data := newTransactionTestData(t)
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: 999999})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: 999999})
if _, err := (&authorityAccessRepo{data: data}).ListAuthorities(ctx); !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatalf("missing current authority error = %v", err)
}
@ -198,7 +198,7 @@ func TestSetAuthorityUsersStrictRejectsUsersOutsideManagedRoles(t *testing.T) {
if err := db.Create(&users).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
if err := repo.SetAuthorityUsers(ctx, childID, []uint{users[1].ID}); err == nil {
t.Fatal("SetAuthorityUsers() accepted a user outside the managed role tree")
@ -231,7 +231,7 @@ func TestSetAuthorityUsersStrictRejectsMixedRoleUserAlreadyLinked(t *testing.T)
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
if err := (&authorityAccessRepo{data: data}).SetAuthorityUsers(ctx, childID, nil); err == nil {
t.Fatal("SetAuthorityUsers() modified a linked user that also has an out-of-scope role")
}
@ -257,8 +257,8 @@ func TestUpdateAuthorityStrictRejectsHierarchyCycle(t *testing.T) {
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
err := (&authorityAccessRepo{data: data}).UpdateAuthority(ctx, &biz.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &childID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
err := (&authorityAccessRepo{data: data}).UpdateAuthority(ctx, &system.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &childID})
if err == nil {
t.Fatal("UpdateAuthority() accepted a parent that forms a cycle")
}
@ -283,7 +283,7 @@ func TestSetDataScopeValidatesScopeAndStrictDepartments(t *testing.T) {
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
if err := repo.SetDataScope(ctx, childID, 0, nil); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("SetDataScope() invalid scope error = %v", err)
@ -323,7 +323,7 @@ func TestSetDataScopeStrictRejectsDepartmentsOutsideActorScope(t *testing.T) {
if err := db.Create(&authorityDepartmentPO{AuthorityID: actorID, DepartmentID: departments[0].ID}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: actorUser.ID, AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{UserID: actorUser.ID, AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
if err := repo.SetDataScope(ctx, childID, 5, []uint{departments[1].ID}); err == nil {
t.Fatal("SetDataScope() accepted a department outside the actor's data scope")
@ -350,31 +350,31 @@ func TestStrictDataScopeGrantRejectsBroaderChildScopes(t *testing.T) {
if err := db.Create(&actorUser).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{UserID: actorUser.ID, AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{UserID: actorUser.ID, AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
created := &biz.Authority{AuthorityID: 1373, AuthorityName: "created", ParentID: &actorID}
created := &system.Authority{AuthorityID: 1373, AuthorityName: "created", ParentID: &actorID}
if err := repo.CreateAuthority(ctx, created); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("CreateAuthority() broader scope error = %v", err)
}
if created.DataScope != 1 {
t.Fatalf("CreateAuthority() did not normalize zero scope before validation: %d", created.DataScope)
}
copied := &biz.Authority{AuthorityID: 1374, AuthorityName: "copied", ParentID: &actorID}
copied := &system.Authority{AuthorityID: 1374, AuthorityName: "copied", ParentID: &actorID}
if err := repo.CopyAuthority(ctx, sourceID, copied); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("CopyAuthority() broader scope error = %v", err)
}
if copied.DataScope != 1 {
t.Fatalf("CopyAuthority() did not normalize zero scope before validation: %d", copied.DataScope)
}
if err := repo.UpdateAuthority(ctx, &biz.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &actorID, DataScope: 2}); !errors.Is(err, errInvalidDataScope) {
if err := repo.UpdateAuthority(ctx, &system.Authority{AuthorityID: targetID, AuthorityName: "target", ParentID: &actorID, DataScope: 2}); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("UpdateAuthority() broader scope error = %v", err)
}
if err := repo.SetDataScope(ctx, targetID, 1, nil); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("SetDataScope() broader scope error = %v", err)
}
allowed := &biz.Authority{AuthorityID: 1375, AuthorityName: "self-only", ParentID: &actorID, DataScope: 4}
allowed := &system.Authority{AuthorityID: 1375, AuthorityName: "self-only", ParentID: &actorID, DataScope: 4}
if err := repo.CreateAuthority(ctx, allowed); err != nil {
t.Fatalf("CreateAuthority() rejected a narrower scope: %v", err)
}
@ -405,9 +405,9 @@ func TestCreateAuthorityStrictDefaultsStayWithinActorPermissions(t *testing.T) {
if err := db.Create(&apiPO{Path: "/menu/getMenu", Method: "POST"}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
first := &biz.Authority{AuthorityID: 1401, AuthorityName: "no-defaults", ParentID: &actorID}
first := &system.Authority{AuthorityID: 1401, AuthorityName: "no-defaults", ParentID: &actorID}
if err := repo.CreateAuthority(ctx, first); err != nil {
t.Fatal(err)
}
@ -429,7 +429,7 @@ func TestCreateAuthorityStrictDefaultsStayWithinActorPermissions(t *testing.T) {
if err := db.Create(&policy).Error; err != nil {
t.Fatal(err)
}
second := &biz.Authority{AuthorityID: 1402, AuthorityName: "owned-defaults", ParentID: &actorID}
second := &system.Authority{AuthorityID: 1402, AuthorityName: "owned-defaults", ParentID: &actorID}
if err := repo.CreateAuthority(ctx, second); err != nil {
t.Fatal(err)
}
@ -476,21 +476,21 @@ func TestCopyAuthorityStrictValidatesCopiedMenusAndButtons(t *testing.T) {
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &authorityAccessRepo{data: data}
if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1510, AuthorityName: "menu-fail", ParentID: &actorID}); err == nil {
if err := repo.CopyAuthority(ctx, sourceID, &system.Authority{AuthorityID: 1510, AuthorityName: "menu-fail", ParentID: &actorID}); err == nil {
t.Fatal("CopyAuthority() copied a menu not assigned to the actor")
}
if err := db.Where("sys_authority_authority_id = ? AND sys_base_menu_id = ?", sourceID, 11).Delete(&authorityMenuPO{}).Error; err != nil {
t.Fatal(err)
}
if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1511, AuthorityName: "button-fail", ParentID: &actorID}); err == nil {
if err := repo.CopyAuthority(ctx, sourceID, &system.Authority{AuthorityID: 1511, AuthorityName: "button-fail", ParentID: &actorID}); err == nil {
t.Fatal("CopyAuthority() copied a button not assigned to the actor")
}
if err := db.Where("authority_id = ? AND sys_base_menu_btn_id = ?", sourceID, 32).Delete(&authorityButtonPO{}).Error; err != nil {
t.Fatal(err)
}
if err := repo.CopyAuthority(ctx, sourceID, &biz.Authority{AuthorityID: 1512, AuthorityName: "valid", ParentID: &actorID}); err != nil {
if err := repo.CopyAuthority(ctx, sourceID, &system.Authority{AuthorityID: 1512, AuthorityName: "valid", ParentID: &actorID}); err != nil {
t.Fatalf("CopyAuthority() rejected owned permissions: %v", err)
}
for _, failedID := range []uint{1510, 1511} {
@ -509,7 +509,7 @@ func TestAuthorityCreateAndCopyNormalizeZeroDataScope(t *testing.T) {
db := data.gormDB.WithContext(context.Background())
repo := &authorityAccessRepo{data: data}
created := &biz.Authority{AuthorityID: 1600, AuthorityName: "created"}
created := &system.Authority{AuthorityID: 1600, AuthorityName: "created"}
if err := repo.CreateAuthority(context.Background(), created); err != nil {
t.Fatal(err)
}
@ -520,7 +520,7 @@ func TestAuthorityCreateAndCopyNormalizeZeroDataScope(t *testing.T) {
if err := db.Create(&authorityPO{AuthorityID: 1601, AuthorityName: "source", DataScope: 3}).Error; err != nil {
t.Fatal(err)
}
copied := &biz.Authority{AuthorityID: 1602, AuthorityName: "copied"}
copied := &system.Authority{AuthorityID: 1602, AuthorityName: "copied"}
if err := repo.CopyAuthority(context.Background(), 1601, copied); err != nil {
t.Fatal(err)
}
@ -549,13 +549,13 @@ func TestAuthorityMutationsRejectInvalidDataScope(t *testing.T) {
}
for _, scope := range []int{-1, 6} {
if err := repo.CreateAuthority(context.Background(), &biz.Authority{AuthorityID: uint(1800 + scope + 1), AuthorityName: "invalid-create", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
if err := repo.CreateAuthority(context.Background(), &system.Authority{AuthorityID: uint(1800 + scope + 1), AuthorityName: "invalid-create", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("CreateAuthority() scope %d error = %v", scope, err)
}
if err := repo.CopyAuthority(context.Background(), 1700, &biz.Authority{AuthorityID: uint(1900 + scope + 1), AuthorityName: "invalid-copy", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
if err := repo.CopyAuthority(context.Background(), 1700, &system.Authority{AuthorityID: uint(1900 + scope + 1), AuthorityName: "invalid-copy", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("CopyAuthority() scope %d error = %v", scope, err)
}
if err := repo.UpdateAuthority(context.Background(), &biz.Authority{AuthorityID: 1701, AuthorityName: "invalid-update", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
if err := repo.UpdateAuthority(context.Background(), &system.Authority{AuthorityID: 1701, AuthorityName: "invalid-update", DataScope: scope}); !errors.Is(err, errInvalidDataScope) {
t.Fatalf("UpdateAuthority() scope %d error = %v", scope, err)
}
}
@ -568,7 +568,7 @@ func TestUpdateAuthorityZeroDataScopeKeepsStoredValue(t *testing.T) {
t.Fatal(err)
}
if err := (&authorityAccessRepo{data: data}).UpdateAuthority(context.Background(), &biz.Authority{AuthorityID: 2000, AuthorityName: "after", DataScope: 0}); err != nil {
if err := (&authorityAccessRepo{data: data}).UpdateAuthority(context.Background(), &system.Authority{AuthorityID: 2000, AuthorityName: "after", DataScope: 0}); err != nil {
t.Fatal(err)
}
var stored authorityPO

View File

@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@ -23,13 +23,13 @@ type dataAccessLogPO struct {
func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" }
func (r *auditRecorderRepo) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error {
func (r *auditRecorderRepo) RecordDataAccess(ctx context.Context, v *system.DataAccessLog) error {
return r.data.DB().WithContext(ctx).Create(&dataAccessLogPO{EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}).Error
}
func dataAccessFromPO(v dataAccessLogPO) *biz.DataAccessLog {
return &biz.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}
func dataAccessFromPO(v dataAccessLogPO) *system.DataAccessLog {
return &system.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}
}
func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]*biz.DataAccessLog, int64, error) {
func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *system.DataAccessLog) ([]*system.DataAccessLog, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&dataAccessLogPO{})
if q != nil {
if q.EventType != "" {
@ -47,7 +47,7 @@ func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *
if err := pagination.ApplyRequired(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.DataAccessLog, 0, len(pos))
out := make([]*system.DataAccessLog, 0, len(pos))
for _, po := range pos {
out = append(out, dataAccessFromPO(po))
}

View File

@ -3,18 +3,17 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"strconv"
"strings"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
type departmentRepo struct{ data Provider }
func NewDepartmentRepo(data Provider) biz.DepartmentRepo { return &departmentRepo{data: data} }
func NewDepartmentRepo(data Provider) system.DepartmentRepo { return &departmentRepo{data: data} }
type departmentPO struct {
ID uint `gorm:"primaryKey"`
@ -47,11 +46,11 @@ type authorityDepartmentPO struct {
func (authorityDepartmentPO) TableName() string { return "sys_authority_departments" }
func deptFromPO(po departmentPO) *biz.Department {
return &biz.Department{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID, Ancestors: po.Ancestors, Sort: po.Sort, LeaderID: po.LeaderID, Status: po.Status}
func deptFromPO(po departmentPO) *system.Department {
return &system.Department{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID, Ancestors: po.Ancestors, Sort: po.Sort, LeaderID: po.LeaderID, Status: po.Status}
}
func (r *departmentRepo) attachDepartmentLeaders(ctx context.Context, departments []*biz.Department) error {
func (r *departmentRepo) attachDepartmentLeaders(ctx context.Context, departments []*system.Department) error {
leaderIDs := make([]uint, 0, len(departments))
for _, department := range departments {
if department.LeaderID != 0 {
@ -65,7 +64,7 @@ func (r *departmentRepo) attachDepartmentLeaders(ctx context.Context, department
if err := r.data.DB().WithContext(ctx).Where("id IN ?", leaderIDs).Find(&leaders).Error; err != nil {
return err
}
leaderByID := make(map[uint]*biz.User, len(leaders))
leaderByID := make(map[uint]*system.User, len(leaders))
for i := range leaders {
leaderByID[leaders[i].ID] = baseBizUser(&leaders[i])
}
@ -74,7 +73,7 @@ func (r *departmentRepo) attachDepartmentLeaders(ctx context.Context, department
}
return nil
}
func (r *departmentRepo) CreateDepartment(ctx context.Context, v *biz.Department) error {
func (r *departmentRepo) CreateDepartment(ctx context.Context, v *system.Department) error {
v.Ancestors = "0"
if v.ParentID != 0 {
var parent departmentPO
@ -88,7 +87,7 @@ func (r *departmentRepo) CreateDepartment(ctx context.Context, v *biz.Department
}
return r.data.DB().WithContext(ctx).Create(&departmentPO{Name: v.Name, ParentID: v.ParentID, Ancestors: v.Ancestors, Sort: v.Sort, LeaderID: v.LeaderID, Status: v.Status}).Error
}
func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *biz.Department) error {
func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *system.Department) error {
if v.ParentID == v.ID {
return errors.New("父部门不能是自己")
}
@ -130,18 +129,18 @@ func (r *departmentRepo) DeleteDepartment(ctx context.Context, id uint) error {
}
return r.data.DB().WithContext(ctx).Delete(&departmentPO{}, id).Error
}
func (r *departmentRepo) FindDepartment(ctx context.Context, id uint) (*biz.Department, error) {
func (r *departmentRepo) FindDepartment(ctx context.Context, id uint) (*system.Department, error) {
var po departmentPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
department := deptFromPO(po)
if err := r.attachDepartmentLeaders(ctx, []*biz.Department{department}); err != nil {
if err := r.attachDepartmentLeaders(ctx, []*system.Department{department}); err != nil {
return nil, err
}
return department, nil
}
func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) {
func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*system.Department, error) {
var pos []departmentPO
db := r.data.DB().WithContext(ctx).Order("sort")
if name != "" {
@ -150,15 +149,15 @@ func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*b
if err := db.Find(&pos).Error; err != nil {
return nil, err
}
nodes := map[uint]*biz.Department{}
items := make([]*biz.Department, 0, len(pos))
nodes := map[uint]*system.Department{}
items := make([]*system.Department, 0, len(pos))
for _, po := range pos {
item := deptFromPO(po)
if name == "" {
// Recursive GORM child queries initialize leaf collections to an empty
// slice. Preserve that tree response shape while search and single-item
// queries keep the model's nil children value.
item.Children = []*biz.Department{}
item.Children = []*system.Department{}
}
nodes[po.ID] = item
items = append(items, item)
@ -169,7 +168,7 @@ func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*b
if name != "" {
return items, nil
}
roots := make([]*biz.Department, 0)
roots := make([]*system.Department, 0)
for _, po := range pos {
n := nodes[po.ID]
if p := nodes[po.ParentID]; p != nil {

View File

@ -4,10 +4,10 @@ import (
"context"
"errors"
"fmt"
"kra/internal/biz/system"
"strings"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@ -15,7 +15,7 @@ import (
type dictionaryRepo struct{ data Provider }
func NewDictionaryRepo(data Provider) biz.DictionaryRepo { return &dictionaryRepo{data: data} }
func NewDictionaryRepo(data Provider) system.DictionaryRepo { return &dictionaryRepo{data: data} }
type dictionaryPO struct {
ID uint `gorm:"primaryKey"`
@ -49,17 +49,17 @@ type dictionaryDetailPO struct {
func (dictionaryDetailPO) TableName() string { return "sys_dictionary_details" }
func dictionaryFromPO(po dictionaryPO) *biz.Dictionary {
return &biz.Dictionary{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Type: po.Type, Status: po.Status, Desc: po.Desc, ParentID: po.ParentID}
func dictionaryFromPO(po dictionaryPO) *system.Dictionary {
return &system.Dictionary{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Type: po.Type, Status: po.Status, Desc: po.Desc, ParentID: po.ParentID}
}
func detailFromPO(po dictionaryDetailPO) *biz.DictionaryDetail {
return &biz.DictionaryDetail{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Label: po.Label, Value: po.Value, Extend: po.Extend, Status: po.Status, Sort: po.Sort, DictionaryID: po.DictionaryID, ParentID: po.ParentID, Level: po.Level, Path: po.Path}
func detailFromPO(po dictionaryDetailPO) *system.DictionaryDetail {
return &system.DictionaryDetail{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Label: po.Label, Value: po.Value, Extend: po.Extend, Status: po.Status, Sort: po.Sort, DictionaryID: po.DictionaryID, ParentID: po.ParentID, Level: po.Level, Path: po.Path}
}
func parameterFromPO(po parameterPO) *biz.SystemParameter {
return &biz.SystemParameter{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Key: po.Key, Value: po.Value, Desc: po.Desc}
func parameterFromPO(po parameterPO) *system.SystemParameter {
return &system.SystemParameter{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Key: po.Key, Value: po.Value, Desc: po.Desc}
}
func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error {
func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *system.Dictionary) error {
var existing dictionaryPO
if err := r.data.DB().WithContext(ctx).Where("type = ?", v.Type).First(&existing).Error; err == nil {
return errors.New("存在相同的type不允许创建")
@ -75,7 +75,7 @@ func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary
return nil
}
func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *biz.Dictionary, details []*biz.DictionaryDetail) error {
func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *system.Dictionary, details []*system.DictionaryDetail) error {
var existing dictionaryPO
if err := r.data.DB().WithContext(ctx).Where("type = ?", dictionary.Type).First(&existing).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("存在相同的type不允许导入")
@ -119,7 +119,7 @@ func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *biz.D
return nil
})
}
func (r *dictionaryRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error {
func (r *dictionaryRepo) UpdateDictionary(ctx context.Context, v *system.Dictionary) error {
db := r.data.DB().WithContext(ctx)
var current dictionaryPO
if err := db.Where("id = ?", v.ID).First(&current).Error; err != nil {
@ -175,7 +175,7 @@ func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error {
}
return db.Where("sys_dictionary_id = ?", id).Delete(&dictionaryDetailPO{}).Error
}
func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) {
func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*system.Dictionary, error) {
var po dictionaryPO
db := r.data.DB().WithContext(ctx)
active := true
@ -192,7 +192,7 @@ func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string
if err = db.Where("sys_dictionary_id = ? AND status = ?", out.ID, true).Order("sort").Find(&detailPOs).Error; err != nil {
return nil, err
}
out.Details = make([]*biz.DictionaryDetail, 0, len(detailPOs))
out.Details = make([]*system.DictionaryDetail, 0, len(detailPOs))
for _, detail := range detailPOs {
out.Details = append(out.Details, detailFromPO(detail))
}
@ -200,7 +200,7 @@ func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string
return out, err
}
func (r *dictionaryRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Dictionary, error) {
func (r *dictionaryRepo) ExportDictionary(ctx context.Context, id uint) (*system.Dictionary, error) {
var po dictionaryPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
@ -210,13 +210,13 @@ func (r *dictionaryRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Di
if err := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ?", id).Order("sort").Find(&detailPOs).Error; err != nil {
return nil, err
}
value.Details = make([]*biz.DictionaryDetail, 0, len(detailPOs))
value.Details = make([]*system.DictionaryDetail, 0, len(detailPOs))
for _, detail := range detailPOs {
value.Details = append(value.Details, detailFromPO(detail))
}
return value, nil
}
func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*biz.Dictionary, int64, error) {
func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*system.Dictionary, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&dictionaryPO{})
if name != "" {
like := "%" + name + "%"
@ -265,16 +265,16 @@ func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, n
detailsByDictionary[value.DictionaryID] = append(detailsByDictionary[value.DictionaryID], value)
}
}
out := make([]*biz.Dictionary, 0, len(pos))
out := make([]*system.Dictionary, 0, len(pos))
for _, po := range pos {
v := dictionaryFromPO(po)
if details {
v.Details = make([]*biz.DictionaryDetail, 0, len(detailsByDictionary[v.ID]))
v.Details = make([]*system.DictionaryDetail, 0, len(detailsByDictionary[v.ID]))
for _, detail := range detailsByDictionary[v.ID] {
v.Details = append(v.Details, detailFromPO(detail))
}
} else {
v.Children = make([]*biz.Dictionary, 0, len(childrenByParent[po.ID]))
v.Children = make([]*system.Dictionary, 0, len(childrenByParent[po.ID]))
for _, child := range childrenByParent[po.ID] {
v.Children = append(v.Children, dictionaryFromPO(child))
}
@ -284,7 +284,7 @@ func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, n
return out, total, nil
}
func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *system.DictionaryDetail) error {
po := dictionaryDetailPO{Label: v.Label, Value: v.Value, Extend: v.Extend, Status: v.Status, Sort: v.Sort, DictionaryID: v.DictionaryID, ParentID: v.ParentID}
po.Level = 0
po.Path = ""
@ -304,7 +304,7 @@ func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.Dict
v.Path = po.Path
return nil
}
func (r *dictionaryRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error {
func (r *dictionaryRepo) UpdateDictionaryDetail(ctx context.Context, v *system.DictionaryDetail) error {
var po dictionaryDetailPO
db := r.data.DB().WithContext(ctx)
if err := db.First(&po, v.ID).Error; err != nil {
@ -359,14 +359,14 @@ func (r *dictionaryRepo) DeleteDictionaryDetail(ctx context.Context, id uint) er
}
return r.data.DB().WithContext(ctx).Delete(&dictionaryDetailPO{}, id).Error
}
func (r *dictionaryRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.DictionaryDetail, error) {
func (r *dictionaryRepo) FindDictionaryDetail(ctx context.Context, id uint) (*system.DictionaryDetail, error) {
var po dictionaryDetailPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return detailFromPO(po), nil
}
func (r *dictionaryRepo) ListDictionaryDetails(ctx context.Context, page, size int, filter biz.DictionaryDetailFilter) ([]*biz.DictionaryDetail, int64, error) {
func (r *dictionaryRepo) ListDictionaryDetails(ctx context.Context, page, size int, filter system.DictionaryDetailFilter) ([]*system.DictionaryDetail, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&dictionaryDetailPO{})
if filter.DictionaryID != 0 {
db = db.Where("sys_dictionary_id = ?", filter.DictionaryID)
@ -394,13 +394,13 @@ func (r *dictionaryRepo) ListDictionaryDetails(ctx context.Context, page, size i
if err := pagination.ApplyRequired(db.Order("sort,id"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.DictionaryDetail, 0, len(pos))
out := make([]*system.DictionaryDetail, 0, len(pos))
for _, po := range pos {
out = append(out, detailFromPO(po))
}
return out, total, nil
}
func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID uint, typ string) ([]*biz.DictionaryDetail, error) {
func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID uint, typ string) ([]*system.DictionaryDetail, error) {
if dictionaryID == 0 {
// The tree-by-type endpoint resolves only by dictionary type. Unlike the
// public dictionary lookup, it does not require the dictionary itself to
@ -415,7 +415,7 @@ func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID
if err := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ? AND parent_id IS NULL", dictionaryID).Order("sort").Find(&pos).Error; err != nil {
return nil, err
}
roots := make([]*biz.DictionaryDetail, 0, len(pos))
roots := make([]*system.DictionaryDetail, 0, len(pos))
for _, po := range pos {
item := detailFromPO(po)
if err := r.loadDictionaryDetailChildren(ctx, item); err != nil {
@ -426,7 +426,7 @@ func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID
return roots, nil
}
func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictionaryID uint, parentID *uint, includeChildren bool) ([]*biz.DictionaryDetail, error) {
func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictionaryID uint, parentID *uint, includeChildren bool) ([]*system.DictionaryDetail, error) {
db := r.data.DB().WithContext(ctx).Where("sys_dictionary_id = ?", dictionaryID)
if parentID == nil {
db = db.Where("parent_id IS NULL")
@ -437,7 +437,7 @@ func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictiona
if err := db.Order("sort").Find(&pos).Error; err != nil {
return nil, err
}
items := make([]*biz.DictionaryDetail, 0, len(pos))
items := make([]*system.DictionaryDetail, 0, len(pos))
for _, po := range pos {
item := detailFromPO(po)
if includeChildren {
@ -450,12 +450,12 @@ func (r *dictionaryRepo) DictionaryDetailsByParent(ctx context.Context, dictiona
return items, nil
}
func (r *dictionaryRepo) loadDictionaryDetailChildren(ctx context.Context, parent *biz.DictionaryDetail) error {
func (r *dictionaryRepo) loadDictionaryDetailChildren(ctx context.Context, parent *system.DictionaryDetail) error {
var pos []dictionaryDetailPO
if err := r.data.DB().WithContext(ctx).Where("parent_id = ?", parent.ID).Order("sort").Find(&pos).Error; err != nil {
return err
}
parent.Children = make([]*biz.DictionaryDetail, 0, len(pos))
parent.Children = make([]*system.DictionaryDetail, 0, len(pos))
for _, po := range pos {
child := detailFromPO(po)
if err := r.loadDictionaryDetailChildren(ctx, child); err != nil {

View File

@ -2,9 +2,8 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
)
func TestDictionaryListPreservesPreloadCollectionShapes(t *testing.T) {
@ -12,10 +11,10 @@ func TestDictionaryListPreservesPreloadCollectionShapes(t *testing.T) {
ctx := context.Background()
repo := &dictionaryRepo{data: data}
if err := repo.CreateDictionary(ctx, &biz.Dictionary{Name: "status", Type: "status"}); err != nil {
if err := repo.CreateDictionary(ctx, &system.Dictionary{Name: "status", Type: "status"}); err != nil {
t.Fatal(err)
}
if err := repo.CreateDictionary(ctx, &biz.Dictionary{Name: "kind", Type: "kind"}); err != nil {
if err := repo.CreateDictionary(ctx, &system.Dictionary{Name: "kind", Type: "kind"}); err != nil {
t.Fatal(err)
}
@ -47,10 +46,10 @@ func TestImportDictionaryRejectsDuplicateType(t *testing.T) {
ctx := context.Background()
repo := &dictionaryRepo{data: data}
if err := repo.CreateDictionary(ctx, &biz.Dictionary{Name: "first", Type: "duplicate"}); err != nil {
if err := repo.CreateDictionary(ctx, &system.Dictionary{Name: "first", Type: "duplicate"}); err != nil {
t.Fatal(err)
}
if err := repo.ImportDictionary(ctx, &biz.Dictionary{Name: "second", Type: "duplicate"}, nil); err == nil || err.Error() != "存在相同的type不允许导入" {
if err := repo.ImportDictionary(ctx, &system.Dictionary{Name: "second", Type: "duplicate"}, nil); err == nil || err.Error() != "存在相同的type不允许导入" {
t.Fatalf("duplicate import error = %v", err)
}
@ -68,16 +67,16 @@ func TestDictionaryTreeAndParentQueriesPreserveLoadedChildrenShape(t *testing.T)
ctx := context.Background()
repo := &dictionaryRepo{data: data}
dictionary := &biz.Dictionary{Name: "tree", Type: "tree"}
dictionary := &system.Dictionary{Name: "tree", Type: "tree"}
if err := repo.CreateDictionary(ctx, dictionary); err != nil {
t.Fatal(err)
}
parent := &biz.DictionaryDetail{Label: "parent", Value: "parent", DictionaryID: dictionary.ID, Sort: 2}
parent := &system.DictionaryDetail{Label: "parent", Value: "parent", DictionaryID: dictionary.ID, Sort: 2}
if err := repo.CreateDictionaryDetail(ctx, parent); err != nil {
t.Fatal(err)
}
parentID := parent.ID
child := &biz.DictionaryDetail{Label: "child", Value: "child", DictionaryID: dictionary.ID, ParentID: &parentID, Sort: 1}
child := &system.DictionaryDetail{Label: "child", Value: "child", DictionaryID: dictionary.ID, ParentID: &parentID, Sort: 1}
if err := repo.CreateDictionaryDetail(ctx, child); err != nil {
t.Fatal(err)
}
@ -112,15 +111,15 @@ func TestDictionaryDetailListAppliesZeroLimit(t *testing.T) {
ctx := context.Background()
repo := &dictionaryRepo{data: data}
dictionary := &biz.Dictionary{Name: "limited", Type: "limited"}
dictionary := &system.Dictionary{Name: "limited", Type: "limited"}
if err := repo.CreateDictionary(ctx, dictionary); err != nil {
t.Fatal(err)
}
if err := repo.CreateDictionaryDetail(ctx, &biz.DictionaryDetail{Label: "one", Value: "one", DictionaryID: dictionary.ID}); err != nil {
if err := repo.CreateDictionaryDetail(ctx, &system.DictionaryDetail{Label: "one", Value: "one", DictionaryID: dictionary.ID}); err != nil {
t.Fatal(err)
}
items, total, err := repo.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: dictionary.ID})
items, total, err := repo.ListDictionaryDetails(ctx, 0, 0, system.DictionaryDetailFilter{DictionaryID: dictionary.ID})
if err != nil {
t.Fatal(err)
}
@ -136,7 +135,7 @@ func TestUserListAppliesZeroLimitForNegativePageSize(t *testing.T) {
t.Fatal(err)
}
items, total, err := (&userRepo{data: data}).ListUsers(ctx, 1, -1, &biz.UserListFilter{})
items, total, err := (&userRepo{data: data}).ListUsers(ctx, 1, -1, &system.UserListFilter{})
if err != nil {
t.Fatal(err)
}
@ -150,7 +149,7 @@ func TestDepartmentTreeAndFlatResultsPreserveChildrenShape(t *testing.T) {
ctx := context.Background()
repo := &departmentRepo{data: data}
root := &biz.Department{Name: "root"}
root := &system.Department{Name: "root"}
if err := repo.CreateDepartment(ctx, root); err != nil {
t.Fatal(err)
}
@ -158,7 +157,7 @@ func TestDepartmentTreeAndFlatResultsPreserveChildrenShape(t *testing.T) {
if err := data.gormDB.WithContext(ctx).Where("name = ?", "root").First(&rootPO).Error; err != nil {
t.Fatal(err)
}
if err := repo.CreateDepartment(ctx, &biz.Department{Name: "child", ParentID: rootPO.ID}); err != nil {
if err := repo.CreateDepartment(ctx, &system.Department{Name: "child", ParentID: rootPO.ID}); err != nil {
t.Fatal(err)
}

View File

@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@ -26,10 +26,10 @@ type errorRecordPO struct {
func (errorRecordPO) TableName() string { return "sys_error" }
func errorFromPO(v errorRecordPO) *biz.ErrorRecord {
return &biz.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
func errorFromPO(v errorRecordPO) *system.ErrorRecord {
return &system.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}
}
func (r *auditRecorderRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error {
func (r *auditRecorderRepo) CreateError(ctx context.Context, v *system.ErrorRecord) error {
if !r.data.DatabaseReady() {
// Silently ignore error records before the database is initialized.
return nil
@ -39,7 +39,7 @@ func (r *auditRecorderRepo) CreateError(ctx context.Context, v *biz.ErrorRecord)
}
return r.data.DB().WithContext(ctx).Create(&errorRecordPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}).Error
}
func (r *auditQueryRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error {
func (r *auditQueryRepo) UpdateError(ctx context.Context, v *system.ErrorRecord) error {
updates := make(map[string]any, 9)
if v.ID != 0 {
updates["id"] = v.ID
@ -73,14 +73,14 @@ func (r *auditQueryRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) er
func (r *auditQueryRepo) DeleteErrors(ctx context.Context, ids []uint) error {
return r.data.DB().WithContext(ctx).Delete(&errorRecordPO{}, ids).Error
}
func (r *auditQueryRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, error) {
func (r *auditQueryRepo) FindError(ctx context.Context, id uint) (*system.ErrorRecord, error) {
var po errorRecordPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return errorFromPO(po), nil
}
func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]*biz.ErrorRecord, int64, error) {
func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *system.ErrorRecord) ([]*system.ErrorRecord, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&errorRecordPO{})
if q != nil {
if len(q.CreatedAtRange) == 2 {
@ -101,7 +101,7 @@ func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *biz.
if err := pagination.Apply(db.Order("created_at desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.ErrorRecord, 0, len(pos))
out := make([]*system.ErrorRecord, 0, len(pos))
for _, po := range pos {
out = append(out, errorFromPO(po))
}

View File

@ -2,10 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"time"
"kra/internal/biz"
)
func errorString(value string) *string { return &value }
@ -41,7 +40,7 @@ func TestCreateErrorBeforeDatabaseInitializationIsNoop(t *testing.T) {
t.Cleanup(func() { _ = sqlDB.Close() })
recorder := &auditRecorderRepo{data: &Data{gormDB: newReloadableDB(db, nil)}}
form := "后端"
if err = recorder.CreateError(context.Background(), &biz.ErrorRecord{Form: &form}); err != nil {
if err = recorder.CreateError(context.Background(), &system.ErrorRecord{Form: &form}); err != nil {
t.Fatalf("uninitialized error write = %v, want nil", err)
}
}
@ -50,7 +49,7 @@ func TestCreateErrorPreservesNullableFieldsAndDefaultStatus(t *testing.T) {
recorder, query := newErrorRecordRepos(t)
createdAt := time.Date(2025, time.March, 4, 5, 6, 7, 0, time.UTC)
form := "前端"
if err := recorder.CreateError(context.Background(), &biz.ErrorRecord{
if err := recorder.CreateError(context.Background(), &system.ErrorRecord{
ID: 41,
CreatedAt: createdAt,
Form: &form,
@ -80,7 +79,7 @@ func TestCreateErrorPreservesNullableFieldsAndDefaultStatus(t *testing.T) {
func TestUpdateErrorDistinguishesOmittedAndExplicitEmptyFields(t *testing.T) {
recorder, query := newErrorRecordRepos(t)
form, info, solution := "后端", "错误内容", "解决方案"
if err := recorder.CreateError(context.Background(), &biz.ErrorRecord{
if err := recorder.CreateError(context.Background(), &system.ErrorRecord{
Form: &form,
Info: &info,
Solution: &solution,
@ -98,7 +97,7 @@ func TestUpdateErrorDistinguishesOmittedAndExplicitEmptyFields(t *testing.T) {
}
id := items[0].ID
newForm := "服务端"
if err = query.UpdateError(context.Background(), &biz.ErrorRecord{ID: id, Form: &newForm}); err != nil {
if err = query.UpdateError(context.Background(), &system.ErrorRecord{ID: id, Form: &newForm}); err != nil {
t.Fatal(err)
}
got, err := query.FindError(context.Background(), id)
@ -110,7 +109,7 @@ func TestUpdateErrorDistinguishesOmittedAndExplicitEmptyFields(t *testing.T) {
}
changedCreatedAt := time.Date(2024, time.January, 2, 3, 4, 5, 0, time.UTC)
if err = query.UpdateError(context.Background(), &biz.ErrorRecord{
if err = query.UpdateError(context.Background(), &system.ErrorRecord{
ID: id,
CreatedAt: changedCreatedAt,
Form: errorString(""),

View File

@ -5,12 +5,13 @@ import (
"encoding/json"
"errors"
"fmt"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"kra/internal/biz"
"kra/internal/biz/system"
"strconv"
"strings"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type exportTemplatePO struct {
@ -59,27 +60,27 @@ func (exportJoinPO) TableName() string { return "sys_export_template_join" }
type exportRepo struct{ data Provider }
func NewExportRepo(data Provider) biz.ExportRepo { return &exportRepo{data: data} }
func exportFromPO(po exportTemplatePO, conditions []exportConditionPO, joins []exportJoinPO) *biz.ExportTemplate {
v := &biz.ExportTemplate{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DBName: po.DBName, Name: po.Name, TableName: po.DBTableName, TemplateID: po.TemplateID, TemplateInfo: po.TemplateInfo, SQL: po.SQL, ImportSQL: po.ImportSQL, Limit: po.Limit, Order: po.Order}
func NewExportRepo(data Provider) system.ExportRepo { return &exportRepo{data: data} }
func exportFromPO(po exportTemplatePO, conditions []exportConditionPO, joins []exportJoinPO) *system.ExportTemplate {
v := &system.ExportTemplate{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DBName: po.DBName, Name: po.Name, TableName: po.DBTableName, TemplateID: po.TemplateID, TemplateInfo: po.TemplateInfo, SQL: po.SQL, ImportSQL: po.ImportSQL, Limit: po.Limit, Order: po.Order}
if conditions != nil {
v.Conditions = make([]biz.ExportCondition, 0, len(conditions))
v.Conditions = make([]system.ExportCondition, 0, len(conditions))
}
for _, x := range conditions {
v.Conditions = append(v.Conditions, biz.ExportCondition{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
v.Conditions = append(v.Conditions, system.ExportCondition{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, From: x.From, Column: x.Column, Operator: x.Operator})
}
if joins != nil {
v.Joins = make([]biz.ExportJoin, 0, len(joins))
v.Joins = make([]system.ExportJoin, 0, len(joins))
}
for _, x := range joins {
v.Joins = append(v.Joins, biz.ExportJoin{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
v.Joins = append(v.Joins, system.ExportJoin{ID: x.ID, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt, TemplateID: x.TemplateID, Join: x.Join, Table: x.Table, On: x.On})
}
return v
}
func exportToPO(v *biz.ExportTemplate) exportTemplatePO {
func exportToPO(v *system.ExportTemplate) exportTemplatePO {
return exportTemplatePO{ID: v.ID, DBName: v.DBName, Name: v.Name, DBTableName: v.TableName, TemplateID: v.TemplateID, TemplateInfo: v.TemplateInfo, SQL: v.SQL, ImportSQL: v.ImportSQL, Limit: v.Limit, Order: v.Order}
}
func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate, resetIDs, replace, forceTemplateID bool) error {
func (r *exportRepo) saveRelations(tx *gorm.DB, v *system.ExportTemplate, resetIDs, replace, forceTemplateID bool) error {
if replace {
if err := tx.Where("template_id = ?", v.TemplateID).Delete(&exportConditionPO{}).Error; err != nil {
return err
@ -137,7 +138,7 @@ func (r *exportRepo) saveRelations(tx *gorm.DB, v *biz.ExportTemplate, resetIDs,
}
return nil
}
func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *system.ExportTemplate) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := exportToPO(v)
if err := tx.Create(&po).Error; err != nil {
@ -147,7 +148,7 @@ func (r *exportRepo) CreateExportTemplate(ctx context.Context, v *biz.ExportTemp
return r.saveRelations(tx, v, false, false, true)
})
}
func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemplate) error {
func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *system.ExportTemplate) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := exportToPO(v)
if err := tx.Model(&exportTemplatePO{}).Where("id = ?", v.ID).Updates(&po).Error; err != nil {
@ -159,7 +160,7 @@ func (r *exportRepo) UpdateExportTemplate(ctx context.Context, v *biz.ExportTemp
func (r *exportRepo) DeleteExportTemplates(ctx context.Context, ids []uint) error {
return r.data.DB().WithContext(ctx).Delete(&[]exportTemplatePO{}, "id IN ?", ids).Error
}
func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*biz.ExportTemplate, error) {
func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string) (*system.ExportTemplate, error) {
var po exportTemplatePO
db := r.data.DB().WithContext(ctx)
var err error
@ -181,7 +182,7 @@ func (r *exportRepo) FindExportTemplate(ctx context.Context, id uint, tid string
}
return exportFromPO(po, conditions, joins), nil
}
func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *biz.ExportTemplate) ([]*biz.ExportTemplate, int64, error) {
func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q *system.ExportTemplate) ([]*system.ExportTemplate, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&exportTemplatePO{})
if q != nil {
if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
@ -210,14 +211,14 @@ func (r *exportRepo) ListExportTemplates(ctx context.Context, page, size int, q
if err := db.Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.ExportTemplate, 0, len(pos))
out := make([]*system.ExportTemplate, 0, len(pos))
for _, po := range pos {
out = append(out, exportFromPO(po, nil, nil))
}
return out, total, nil
}
func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) ([]map[string]any, string, error) {
func (r *exportRepo) QueryExport(ctx context.Context, t *system.ExportTemplate, params map[string]string) ([]map[string]any, string, error) {
selected, err := r.data.Database(t.DBName)
if err != nil {
return nil, "", err
@ -327,7 +328,7 @@ func (r *exportRepo) QueryExport(ctx context.Context, t *biz.ExportTemplate, par
return rows, preview, err
}
func (r *exportRepo) PreviewExport(ctx context.Context, t *biz.ExportTemplate, params map[string]string) (string, error) {
func (r *exportRepo) PreviewExport(ctx context.Context, t *system.ExportTemplate, params map[string]string) (string, error) {
columns := make([]string, 0)
decoder := json.NewDecoder(strings.NewReader(t.TemplateInfo))
if token, err := decoder.Token(); err != nil || token != json.Delim('{') {
@ -437,7 +438,7 @@ func parseTemplateColumns(raw string) map[string]string {
}
return out
}
func (r *exportRepo) ImportExportRows(ctx context.Context, t *biz.ExportTemplate, rows []map[string]any) error {
func (r *exportRepo) ImportExportRows(ctx context.Context, t *system.ExportTemplate, rows []map[string]any) error {
// ImportSQL is checked verbatim. In particular, a whitespace-only value
// is still treated as custom SQL and is allowed to return the driver's
// native error instead of silently falling back to GORM insertion.

View File

@ -4,10 +4,10 @@ import (
"context"
"encoding/json"
"errors"
"kra/internal/biz/system"
"strings"
"time"
"kra/internal/biz"
"kra/internal/integration/runtimeconfig"
"gorm.io/gorm"
@ -31,23 +31,23 @@ type integrationRuntimeProvider interface {
IntegrationRuntime() *runtimeconfig.Store
}
func NewIntegrationConfigRepo(data Provider) biz.IntegrationConfigRepo {
func NewIntegrationConfigRepo(data Provider) system.IntegrationConfigRepo {
return &integrationConfigRepo{data: data}
}
func (r *integrationConfigRepo) ListIntegrationConfigs(ctx context.Context, kind string) ([]*biz.IntegrationConfig, error) {
func (r *integrationConfigRepo) ListIntegrationConfigs(ctx context.Context, kind string) ([]*system.IntegrationConfig, error) {
var rows []integrationConfigPO
if err := r.data.DB().WithContext(ctx).Where("kind = ?", kind).Order("provider ASC").Find(&rows).Error; err != nil {
return nil, err
}
result := make([]*biz.IntegrationConfig, 0, len(rows))
result := make([]*system.IntegrationConfig, 0, len(rows))
for _, row := range rows {
result = append(result, integrationConfigFromPO(row))
}
return result, nil
}
func (r *integrationConfigRepo) FindIntegrationConfig(ctx context.Context, kind, provider string) (*biz.IntegrationConfig, error) {
func (r *integrationConfigRepo) FindIntegrationConfig(ctx context.Context, kind, provider string) (*system.IntegrationConfig, error) {
var row integrationConfigPO
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", kind, provider).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@ -58,14 +58,14 @@ func (r *integrationConfigRepo) FindIntegrationConfig(ctx context.Context, kind,
return integrationConfigFromPO(row), nil
}
func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, config *biz.IntegrationConfig) error {
func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, config *system.IntegrationConfig) error {
db := r.data.DB().WithContext(ctx)
var row integrationConfigPO
err := db.Where("kind = ? AND provider = ?", config.Kind, config.Provider).First(&row).Error
values := integrationObject(config.Values)
if errors.Is(err, gorm.ErrRecordNotFound) {
if config.Enabled {
if err = biz.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
if err = system.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
return err
}
}
@ -81,7 +81,7 @@ func (r *integrationConfigRepo) SaveIntegrationConfig(ctx context.Context, confi
}
mergeIntegrationSecrets(config.Kind, config.Provider, values, integrationObject(json.RawMessage(row.Config)))
if config.Enabled {
if err = biz.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
if err = system.ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil {
return err
}
}
@ -116,11 +116,11 @@ func integrationRuntime(provider Provider) *runtimeconfig.Store {
return nil
}
func integrationConfigFromPO(row integrationConfigPO) *biz.IntegrationConfig {
func integrationConfigFromPO(row integrationConfigPO) *system.IntegrationConfig {
values := integrationObject(json.RawMessage(row.Config))
maskIntegrationSecrets(row.Kind, row.Provider, values)
encoded, _ := json.Marshal(values)
return &biz.IntegrationConfig{Kind: row.Kind, Provider: row.Provider, Enabled: row.Enabled, Values: encoded}
return &system.IntegrationConfig{Kind: row.Kind, Provider: row.Provider, Enabled: row.Enabled, Values: encoded}
}
func integrationObject(raw json.RawMessage) map[string]any {
@ -165,7 +165,7 @@ func mergeIntegrationSecrets(kind, provider string, values, old map[string]any)
func integrationSecretFields(kind, provider string) map[string]bool {
result := map[string]bool{}
if definition, ok := biz.IntegrationDefinition(kind, provider); ok {
if definition, ok := system.IntegrationDefinition(kind, provider); ok {
for _, field := range definition.Fields {
if field.Secret {
result[field.Key] = true

View File

@ -3,9 +3,9 @@ package system
import (
"context"
"encoding/json"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
"kra/internal/integration/runtimeconfig"
)
@ -27,19 +27,19 @@ func TestIntegrationConfigSavePublishesUnmaskedRuntimeValues(t *testing.T) {
provider := &integrationRuntimeTestProvider{Data: &Data{gormDB: newReloadableDB(db, nil)}, store: runtimeconfig.NewStore()}
repo := &integrationConfigRepo{data: provider}
values := biz.DefaultIntegrationConfig(biz.IntegrationKindMQ, "rabbitmq")
values := system.DefaultIntegrationConfig(system.IntegrationKindMQ, "rabbitmq")
values["password"] = "runtime-secret"
raw, _ := json.Marshal(values)
if err = repo.SaveIntegrationConfig(context.Background(), &biz.IntegrationConfig{Kind: biz.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
if err = repo.SaveIntegrationConfig(context.Background(), &system.IntegrationConfig{Kind: system.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
t.Fatal(err)
}
values["password"] = "******"
raw, _ = json.Marshal(values)
if err = repo.SaveIntegrationConfig(context.Background(), &biz.IntegrationConfig{Kind: biz.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
if err = repo.SaveIntegrationConfig(context.Background(), &system.IntegrationConfig{Kind: system.IntegrationKindMQ, Provider: "rabbitmq", Enabled: true, Values: raw}); err != nil {
t.Fatal(err)
}
active, ok := provider.store.Get(biz.IntegrationKindMQ, "rabbitmq")
active, ok := provider.store.Get(system.IntegrationKindMQ, "rabbitmq")
if !ok || !active.Enabled {
t.Fatalf("runtime config = %#v, ok=%v", active, ok)
}

View File

@ -7,14 +7,13 @@ import (
"fmt"
"io"
"io/fs"
"kra/internal/biz/system"
"os"
pathpkg "path"
"path/filepath"
"sort"
"strings"
"time"
"kra/internal/biz"
)
const (
@ -26,21 +25,21 @@ const (
func (r *logFileRepo) configuredLogRoot() (root string, exists bool, err error) {
admin := r.data.Runtime().Admin()
if admin == nil || admin.Zap == nil || strings.TrimSpace(admin.Zap.Director) == "" {
return "", false, biz.ErrLogRootUnavailable
return "", false, system.ErrLogRootUnavailable
}
root, err = filepath.Abs(admin.Zap.Director)
if err != nil {
return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return "", false, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
info, err := os.Stat(root)
if errors.Is(err, fs.ErrNotExist) {
return root, false, nil
}
if err != nil {
return "", false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return "", false, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
if !info.IsDir() {
return "", false, biz.ErrLogRootUnavailable
return "", false, system.ErrLogRootUnavailable
}
return root, true, nil
}
@ -52,12 +51,12 @@ func (r *logFileRepo) openConfiguredLogRoot() (root *os.Root, exists bool, err e
}
root, err = os.OpenRoot(rootPath)
if err != nil {
return nil, false, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return nil, false, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
return root, true, nil
}
func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) {
func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]system.LogDate, error) {
if err := validateLogMonth(month); err != nil {
return nil, err
}
@ -66,15 +65,15 @@ func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate
return nil, err
}
if !exists {
return []biz.LogDate{}, nil
return []system.LogDate{}, nil
}
defer logRoot.Close()
entries, err := fs.ReadDir(logRoot.FS(), ".")
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
result := make([]biz.LogDate, 0)
result := make([]system.LogDate, 0)
for _, entry := range entries {
if err = ctx.Err(); err != nil {
return nil, err
@ -90,14 +89,14 @@ func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate
return nil, countErr
}
if count > 0 {
result = append(result, biz.LogDate{Date: entry.Name(), FileCount: count})
result = append(result, system.LogDate{Date: entry.Name(), FileCount: count})
}
}
sort.Slice(result, func(i, j int) bool { return result[i].Date < result[j].Date })
return result, nil
}
func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) {
func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]system.LogFile, error) {
if err := validateLogDate(date); err != nil {
return nil, err
}
@ -106,27 +105,27 @@ func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile,
return nil, err
}
if !exists {
return []biz.LogFile{}, nil
return []system.LogFile{}, nil
}
defer logRoot.Close()
info, err := logRoot.Lstat(date)
if errors.Is(err, fs.ErrNotExist) {
return []biz.LogFile{}, nil
return []system.LogFile{}, nil
}
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
return []biz.LogFile{}, nil
return []system.LogFile{}, nil
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
defer dateRoot.Close()
result := make([]biz.LogFile, 0)
result := make([]system.LogFile, 0)
err = fs.WalkDir(dateRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
@ -151,24 +150,24 @@ func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile,
return infoErr
}
if fileInfo.Mode().IsRegular() {
result = append(result, biz.LogFile{Path: path, Name: entry.Name(), Size: fileInfo.Size(), ModifiedAt: fileInfo.ModTime()})
result = append(result, system.LogFile{Path: path, Name: entry.Name(), Size: fileInfo.Size(), ModifiedAt: fileInfo.ModTime()})
}
return nil
})
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
sort.Slice(result, func(i, j int) bool { return result[i].Path < result[j].Path })
return result, nil
}
func (r *logFileRepo) LogContent(ctx context.Context, date, apiPath string, cursor *int64) (*biz.LogContent, error) {
result := &biz.LogContent{Date: date, Path: apiPath}
func (r *logFileRepo) LogContent(ctx context.Context, date, apiPath string, cursor *int64) (*system.LogContent, error) {
result := &system.LogContent{Date: date, Path: apiPath}
if err := ctx.Err(); err != nil {
return nil, err
}
if cursor != nil && *cursor < 0 {
return nil, biz.ErrInvalidLogPath
return nil, system.ErrInvalidLogPath
}
file, info, err := r.openValidatedLogFile(date, apiPath)
if err != nil {
@ -182,19 +181,19 @@ func (r *logFileRepo) LogContent(ctx context.Context, date, apiPath string, curs
}
start, limitedByBytes, err := findLogChunkStart(file, end)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
data, err := readLogRange(file, start, end)
if err != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
if int64(len(data)) < end-start {
currentInfo, statErr := file.Stat()
if statErr != nil || currentInfo.Size() < start {
if statErr != nil {
return nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr)
return nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, statErr)
}
return nil, biz.ErrLogFileUnreadable
return nil, system.ErrLogFileUnreadable
}
info = currentInfo
}
@ -221,23 +220,23 @@ func (r *logFileRepo) openValidatedLogFile(date, apiPath string) (file *os.File,
return nil, nil, err
}
if !exists {
return nil, nil, biz.ErrLogFileNotFound
return nil, nil, system.ErrLogFileNotFound
}
defer logRoot.Close()
dateInfo, err := logRoot.Lstat(date)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
return nil, nil, system.ErrLogFileNotFound
}
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
if dateInfo.Mode()&os.ModeSymlink != 0 || !dateInfo.IsDir() {
return nil, nil, biz.ErrInvalidLogPath
return nil, nil, system.ErrInvalidLogPath
}
dateRoot, err := logRoot.OpenRoot(date)
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
defer dateRoot.Close()
@ -247,35 +246,35 @@ func (r *logFileRepo) openValidatedLogFile(date, apiPath string) (file *os.File,
relativePath = filepath.Join(relativePath, segment)
validatedInfo, err = dateRoot.Lstat(relativePath)
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
return nil, nil, system.ErrLogFileNotFound
}
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
if validatedInfo.Mode()&os.ModeSymlink != 0 {
return nil, nil, biz.ErrInvalidLogPath
return nil, nil, system.ErrInvalidLogPath
}
if index < len(segments)-1 && !validatedInfo.IsDir() {
return nil, nil, biz.ErrInvalidLogPath
return nil, nil, system.ErrInvalidLogPath
}
}
if validatedInfo == nil || !validatedInfo.Mode().IsRegular() {
return nil, nil, biz.ErrInvalidLogPath
return nil, nil, system.ErrInvalidLogPath
}
file, err = dateRoot.Open(relativePath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil, nil, biz.ErrLogFileNotFound
return nil, nil, system.ErrLogFileNotFound
}
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, err)
return nil, nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, err)
}
openedInfo, statErr := file.Stat()
if statErr != nil || !openedInfo.Mode().IsRegular() || !os.SameFile(validatedInfo, openedInfo) {
file.Close()
if statErr != nil {
return nil, nil, fmt.Errorf("%w: %v", biz.ErrLogFileUnreadable, statErr)
return nil, nil, fmt.Errorf("%w: %v", system.ErrLogFileUnreadable, statErr)
}
return nil, nil, biz.ErrInvalidLogPath
return nil, nil, system.ErrInvalidLogPath
}
return file, openedInfo, nil
}
@ -283,7 +282,7 @@ func (r *logFileRepo) openValidatedLogFile(date, apiPath string) (file *os.File,
func validateLogMonth(month string) error {
parsed, err := time.Parse("2006-01", month)
if err != nil || parsed.Format("2006-01") != month {
return biz.ErrInvalidLogMonth
return system.ErrInvalidLogMonth
}
return nil
}
@ -291,23 +290,23 @@ func validateLogMonth(month string) error {
func validateLogDate(date string) error {
parsed, err := time.Parse("2006-01-02", date)
if err != nil || parsed.Format("2006-01-02") != date {
return biz.ErrInvalidLogDate
return system.ErrInvalidLogDate
}
return nil
}
func validateLogAPIPath(apiPath string) ([]string, error) {
if apiPath == "" || strings.Contains(apiPath, "\\") || strings.Contains(apiPath, ":") || pathpkg.IsAbs(apiPath) {
return nil, biz.ErrInvalidLogPath
return nil, system.ErrInvalidLogPath
}
segments := strings.Split(apiPath, "/")
for _, segment := range segments {
if segment == "" || segment == "." || segment == ".." {
return nil, biz.ErrInvalidLogPath
return nil, system.ErrInvalidLogPath
}
}
if !strings.EqualFold(pathpkg.Ext(apiPath), ".log") {
return nil, biz.ErrInvalidLogPath
return nil, system.ErrInvalidLogPath
}
return segments, nil
}
@ -351,7 +350,7 @@ func countLogFiles(ctx context.Context, logRoot *os.Root, date string) (count in
return nil
})
if err != nil {
return 0, fmt.Errorf("%w: %v", biz.ErrLogRootUnavailable, err)
return 0, fmt.Errorf("%w: %v", system.ErrLogRootUnavailable, err)
}
return count, nil
}

View File

@ -3,11 +3,11 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"os"
"path/filepath"
"testing"
"kra/internal/biz"
"kra/internal/conf"
)
@ -44,7 +44,7 @@ func TestLogViewerRejectsPathTraversal(t *testing.T) {
runtime := conf.NewRuntime(nil, &conf.AdminBackend{Zap: &conf.AdminBackend_Zap{Director: t.TempDir()}})
repo := &logFileRepo{data: &Data{runtime: runtime}}
_, err := repo.LogContent(context.Background(), "2026-08-16", "../application.log", nil)
if !errors.Is(err, biz.ErrInvalidLogPath) {
if !errors.Is(err, system.ErrInvalidLogPath) {
t.Fatalf("expected invalid path error, got %v", err)
}
}

View File

@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@ -23,7 +23,7 @@ type loginLogPO struct {
func (loginLogPO) TableName() string { return "sys_login_logs" }
func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error {
func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *system.LoginLog) error {
if !r.data.DatabaseReady() {
// The login endpoint remains reachable before database initialization;
// skip the audit write until storage is ready.
@ -31,10 +31,10 @@ func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) er
}
return r.data.DB().WithContext(ctx).Create(&loginLogPO{Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}).Error
}
func loginFromPO(v loginLogPO) *biz.LoginLog {
return &biz.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}
func loginFromPO(v loginLogPO) *system.LoginLog {
return &system.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}
}
func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*biz.LoginLog, int64, error) {
func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *system.LoginLog) ([]*system.LoginLog, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&loginLogPO{})
if q != nil {
if q.Username != "" {
@ -59,7 +59,7 @@ func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.
}
}
users := auditUsers(ctx, r.data.DB().WithContext(ctx), ids)
out := make([]*biz.LoginLog, 0, len(pos))
out := make([]*system.LoginLog, 0, len(pos))
for _, po := range pos {
value := loginFromPO(po)
value.User = users[po.UserID]
@ -67,7 +67,7 @@ func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.
}
return out, total, nil
}
func (r *auditQueryRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog, error) {
func (r *auditQueryRepo) FindLogin(ctx context.Context, id uint) (*system.LoginLog, error) {
var po loginLogPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err

View File

@ -3,10 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
@ -41,14 +40,14 @@ func (categoryPO) TableName() string { return "media_attachment_category" }
type mediaRepo struct{ data Provider }
func NewMediaRepo(data Provider) biz.MediaRepo { return &mediaRepo{data: data} }
func mediaFromPO(v mediaPO) *biz.MediaFile {
return &biz.MediaFile{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, CategoryID: v.CategoryID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: v.UserID}
func NewMediaRepo(data Provider) system.MediaRepo { return &mediaRepo{data: data} }
func mediaFromPO(v mediaPO) *system.MediaFile {
return &system.MediaFile{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, CategoryID: v.CategoryID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: v.UserID}
}
func mediaToPO(v *biz.MediaFile) mediaPO {
func mediaToPO(v *system.MediaFile) mediaPO {
return mediaPO{ID: v.ID, Name: v.Name, CategoryID: v.CategoryID, URL: v.URL, Tag: v.Tag, Key: v.Key, Size: v.Size, Mime: v.Mime, MD5: v.MD5, UserID: v.UserID}
}
func (r *mediaRepo) CreateMedia(ctx context.Context, v *biz.MediaFile) error {
func (r *mediaRepo) CreateMedia(ctx context.Context, v *system.MediaFile) error {
po := mediaToPO(v)
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@ -58,21 +57,21 @@ func (r *mediaRepo) CreateMedia(ctx context.Context, v *biz.MediaFile) error {
v.UpdatedAt = po.UpdatedAt
return nil
}
func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*biz.MediaFile, error) {
func (r *mediaRepo) FindMedia(ctx context.Context, id uint) (*system.MediaFile, error) {
var po mediaPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return mediaFromPO(po), nil
}
func (r *mediaRepo) FindMediaByHash(ctx context.Context, userID uint, hash string) (*biz.MediaFile, error) {
func (r *mediaRepo) FindMediaByHash(ctx context.Context, userID uint, hash string) (*system.MediaFile, error) {
var po mediaPO
if err := r.data.DB().WithContext(ctx).Where("user_id = ? AND md5 = ?", userID, hash).First(&po).Error; err != nil {
return nil, err
}
return mediaFromPO(po), nil
}
func (r *mediaRepo) ListMedia(ctx context.Context, filter biz.MediaFilter) ([]*biz.MediaFile, int64, error) {
func (r *mediaRepo) ListMedia(ctx context.Context, filter system.MediaFilter) ([]*system.MediaFile, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&mediaPO{})
if filter.Keyword != "" {
db = db.Where("name LIKE ?", "%"+filter.Keyword+"%")
@ -119,7 +118,7 @@ func (r *mediaRepo) ListMedia(ctx context.Context, filter biz.MediaFilter) ([]*b
if err := db.Limit(limit).Offset(offset).Order(orderKey).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.MediaFile, 0, len(pos))
out := make([]*system.MediaFile, 0, len(pos))
for _, po := range pos {
out = append(out, mediaFromPO(po))
}
@ -142,14 +141,14 @@ func (r *mediaRepo) MediaKeyReferences(ctx context.Context, key string) (int64,
err := r.data.DB().WithContext(ctx).Model(&mediaPO{}).Where(map[string]any{"key": key}).Count(&count).Error
return count, err
}
func (r *mediaRepo) CreateMediaBatch(ctx context.Context, items []*biz.MediaFile) error {
func (r *mediaRepo) CreateMediaBatch(ctx context.Context, items []*system.MediaFile) error {
pos := make([]mediaPO, 0, len(items))
for _, v := range items {
pos = append(pos, mediaToPO(v))
}
return r.data.DB().WithContext(ctx).Create(&pos).Error
}
func (r *mediaRepo) SaveCategory(ctx context.Context, v *biz.AttachmentCategory) error {
func (r *mediaRepo) SaveCategory(ctx context.Context, v *system.AttachmentCategory) error {
db := r.data.DB().WithContext(ctx)
var count int64
query := db.Model(&categoryPO{}).Where("name = ? AND pid = ?", v.Name, v.ParentID)
@ -179,17 +178,17 @@ func (r *mediaRepo) DeleteCategory(ctx context.Context, id uint) error {
}
return r.data.DB().WithContext(ctx).Unscoped().Delete(&categoryPO{}, id).Error
}
func (r *mediaRepo) ListCategories(ctx context.Context) ([]*biz.AttachmentCategory, error) {
func (r *mediaRepo) ListCategories(ctx context.Context) ([]*system.AttachmentCategory, error) {
var pos []categoryPO
if err := r.data.DB().WithContext(ctx).Find(&pos).Error; err != nil {
return nil, err
}
byID := map[uint]*biz.AttachmentCategory{}
byID := map[uint]*system.AttachmentCategory{}
for _, po := range pos {
// Children remains nil for leaf nodes, which serializes as null.
byID[po.ID] = &biz.AttachmentCategory{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID}
byID[po.ID] = &system.AttachmentCategory{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID}
}
roots := []*biz.AttachmentCategory{}
roots := []*system.AttachmentCategory{}
for _, po := range pos {
v := byID[po.ID]
if parent := byID[po.ParentID]; parent != nil {

View File

@ -2,20 +2,19 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
)
func TestMediaListKeepsZeroPageSizeContract(t *testing.T) {
data := newTransactionTestData(t)
repo := &mediaRepo{data: data}
ctx := context.Background()
if err := repo.CreateMedia(ctx, &biz.MediaFile{Name: "one.txt"}); err != nil {
if err := repo.CreateMedia(ctx, &system.MediaFile{Name: "one.txt"}); err != nil {
t.Fatal(err)
}
items, total, err := repo.ListMedia(ctx, biz.MediaFilter{})
items, total, err := repo.ListMedia(ctx, system.MediaFilter{})
if err != nil {
t.Fatal(err)
}

View File

@ -3,10 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
@ -42,30 +41,30 @@ type uploadChunkPO struct {
func (uploadChunkPO) TableName() string { return "media_upload_chunks" }
func uploadFromPO(v uploadSessionPO) *biz.UploadSession {
return &biz.UploadSession{ID: v.ID, UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status, StorageKey: v.StorageKey, MediaID: v.MediaID}
func uploadFromPO(v uploadSessionPO) *system.UploadSession {
return &system.UploadSession{ID: v.ID, UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status, StorageKey: v.StorageKey, MediaID: v.MediaID}
}
func (r *mediaRepo) FindCompletedSession(ctx context.Context, userID uint, hash string) (*biz.UploadSession, error) {
func (r *mediaRepo) FindCompletedSession(ctx context.Context, userID uint, hash string) (*system.UploadSession, error) {
var po uploadSessionPO
if err := r.data.DB().WithContext(ctx).Where("user_id = ? AND file_hash = ? AND status = ?", userID, hash, "completed").First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUploadSessionNotFound
return nil, system.ErrUploadSessionNotFound
}
return nil, err
}
return uploadFromPO(po), nil
}
func (r *mediaRepo) FindUploadingSession(ctx context.Context, userID uint, hash string) (*biz.UploadSession, error) {
func (r *mediaRepo) FindUploadingSession(ctx context.Context, userID uint, hash string) (*system.UploadSession, error) {
var po uploadSessionPO
if err := r.data.DB().WithContext(ctx).Where("user_id = ? AND file_hash = ? AND status = ?", userID, hash, "uploading").First(&po).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrUploadSessionNotFound
return nil, system.ErrUploadSessionNotFound
}
return nil, err
}
return uploadFromPO(po), nil
}
func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *biz.UploadSession) error {
func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *system.UploadSession) error {
po := uploadSessionPO{UserID: v.UserID, FileName: v.FileName, FileHash: v.FileHash, FileSize: v.FileSize, ChunkSize: v.ChunkSize, ChunkTotal: v.ChunkTotal, Status: v.Status}
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@ -73,7 +72,7 @@ func (r *mediaRepo) CreateUploadSession(ctx context.Context, v *biz.UploadSessio
v.ID = po.ID
return nil
}
func (r *mediaRepo) FindUploadSession(ctx context.Context, id uint) (*biz.UploadSession, error) {
func (r *mediaRepo) FindUploadSession(ctx context.Context, id uint) (*system.UploadSession, error) {
var po uploadSessionPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
@ -95,21 +94,21 @@ func (r *mediaRepo) DeleteUploadSession(ctx context.Context, id uint) error {
// for audit/recovery rather than physically removing it.
return r.data.DB().WithContext(ctx).Delete(&uploadSessionPO{}, id).Error
}
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *biz.UploadChunk) error {
func (r *mediaRepo) UpsertChunk(ctx context.Context, uploadID uint, v *system.UploadChunk) error {
po := uploadChunkPO{UploadID: uploadID, ChunkIndex: v.Index, ChunkHash: v.Hash, Size: v.Size}
return r.data.DB().WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "upload_id"}, {Name: "chunk_index"}},
DoUpdates: clause.AssignmentColumns([]string{"chunk_hash", "size", "updated_at", "deleted_at"}),
}).Create(&po).Error
}
func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*biz.UploadChunk, error) {
func (r *mediaRepo) ListChunks(ctx context.Context, uploadID uint) ([]*system.UploadChunk, error) {
var pos []uploadChunkPO
if err := r.data.DB().WithContext(ctx).Where("upload_id = ?", uploadID).Order("chunk_index").Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.UploadChunk, 0, len(pos))
out := make([]*system.UploadChunk, 0, len(pos))
for _, po := range pos {
out = append(out, &biz.UploadChunk{Index: po.ChunkIndex, Hash: po.ChunkHash, Size: po.Size})
out = append(out, &system.UploadChunk{Index: po.ChunkIndex, Hash: po.ChunkHash, Size: po.Size})
}
return out, nil
}

View File

@ -3,31 +3,30 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
type menuRepo struct{ data Provider }
func NewMenuRepo(data Provider) biz.MenuRepo { return &menuRepo{data: data} }
func NewMenuRepo(data Provider) system.MenuRepo { return &menuRepo{data: data} }
func menuFromPO(po menuPO) *biz.Menu {
func menuFromPO(po menuPO) *system.Menu {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.Menu{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType}
return &system.Menu{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, ParentID: po.ParentID, Path: po.Path, Name: po.Name, Hidden: po.Hidden, Component: po.Component, Sort: po.Sort, ActiveName: po.ActiveName, KeepAlive: po.KeepAlive, DefaultMenu: po.DefaultMenu, Title: po.Title, Icon: po.Icon, CloseTab: po.CloseTab, TransitionType: po.TransitionType}
}
func menuToPO(v *biz.Menu) menuPO {
func menuToPO(v *system.Menu) menuPO {
return menuPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, ParentID: v.ParentID, Path: v.Path, Name: v.Name, Hidden: v.Hidden, Component: v.Component, Sort: v.Sort, ActiveName: v.ActiveName, KeepAlive: v.KeepAlive, DefaultMenu: v.DefaultMenu, Title: v.Title, Icon: v.Icon, CloseTab: v.CloseTab, TransitionType: v.TransitionType}
}
func replaceMenuRelations(tx *gorm.DB, menu *biz.Menu) error {
func replaceMenuRelations(tx *gorm.DB, menu *system.Menu) error {
if err := tx.Unscoped().Where("sys_base_menu_id = ?", menu.ID).Delete(&menuParameterPO{}).Error; err != nil {
return err
}
@ -37,7 +36,7 @@ func replaceMenuRelations(tx *gorm.DB, menu *biz.Menu) error {
return createMenuRelations(tx, menu)
}
func createMenuRelations(tx *gorm.DB, menu *biz.Menu) error {
func createMenuRelations(tx *gorm.DB, menu *system.Menu) error {
parameters := make([]menuParameterPO, 0, len(menu.Parameters))
for _, parameter := range menu.Parameters {
parameters = append(parameters, menuParameterPO{ID: parameter.ID, CreatedAt: parameter.CreatedAt, UpdatedAt: parameter.UpdatedAt, MenuID: menu.ID, Type: parameter.Type, Key: parameter.Key, Value: parameter.Value})
@ -57,7 +56,7 @@ func createMenuRelations(tx *gorm.DB, menu *biz.Menu) error {
return nil
}
func (r *menuRepo) Create(ctx context.Context, v *biz.Menu) error {
func (r *menuRepo) Create(ctx context.Context, v *system.Menu) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
if err := tx.Model(&menuPO{}).Where("name = ?", v.Name).Count(&count).Error; err != nil {
@ -98,7 +97,7 @@ func (r *menuRepo) Create(ctx context.Context, v *biz.Menu) error {
})
}
func (r *menuRepo) Update(ctx context.Context, v *biz.Menu) error {
func (r *menuRepo) Update(ctx context.Context, v *system.Menu) error {
return r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var old menuPO
if err := tx.First(&old, v.ID).Error; err != nil {
@ -156,12 +155,12 @@ func (r *menuRepo) Delete(ctx context.Context, id uint) error {
})
}
func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error {
func (r *menuRepo) loadRelations(ctx context.Context, menu *system.Menu) error {
var parameters []menuParameterPO
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Find(&parameters).Error; err != nil {
return err
}
menu.Parameters = make([]*biz.MenuParameter, 0, len(parameters))
menu.Parameters = make([]*system.MenuParameter, 0, len(parameters))
for _, parameter := range parameters {
menu.Parameters = append(menu.Parameters, menuParameterFromPO(parameter))
}
@ -169,14 +168,14 @@ func (r *menuRepo) loadRelations(ctx context.Context, menu *biz.Menu) error {
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menu.ID).Find(&buttons).Error; err != nil {
return err
}
menu.Buttons = make([]*biz.MenuButton, 0, len(buttons))
menu.Buttons = make([]*system.MenuButton, 0, len(buttons))
for _, button := range buttons {
menu.Buttons = append(menu.Buttons, menuButtonFromPO(button))
}
return nil
}
func (r *menuRepo) Find(ctx context.Context, id uint) (*biz.Menu, error) {
func (r *menuRepo) Find(ctx context.Context, id uint) (*system.Menu, error) {
var po menuPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
@ -188,11 +187,11 @@ func (r *menuRepo) Find(ctx context.Context, id uint) (*biz.Menu, error) {
return menu, nil
}
func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
func (r *menuRepo) List(ctx context.Context) ([]*system.Menu, error) {
var pos []menuPO
db := r.data.DB().WithContext(ctx).Model(&menuPO{}).Order("sort")
config := r.data.Runtime().Admin()
if actor, ok := biz.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
if actor, ok := system.ActorFromContext(ctx); ok && config != nil && config.System != nil && config.System.UseStrictAuth {
var authority authorityPO
if err := r.data.DB().WithContext(ctx).Where("authority_id = ?", actor.AuthorityID).First(&authority).Error; err != nil {
return nil, err
@ -204,20 +203,20 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
if err := db.Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.Menu, 0, len(pos))
out := make([]*system.Menu, 0, len(pos))
for _, po := range pos {
out = append(out, menuFromPO(po))
}
if len(out) == 0 {
return out, nil
}
byID := make(map[uint]*biz.Menu, len(out))
byID := make(map[uint]*system.Menu, len(out))
ids := make([]uint, 0, len(out))
for _, menu := range out {
byID[menu.ID] = menu
ids = append(ids, menu.ID)
menu.Parameters = []*biz.MenuParameter{}
menu.Buttons = []*biz.MenuButton{}
menu.Parameters = []*system.MenuParameter{}
menu.Buttons = []*system.MenuButton{}
}
var parameters []menuParameterPO
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id IN ?", ids).Find(&parameters).Error; err != nil {
@ -238,22 +237,22 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) {
return out, nil
}
func menuParameterFromPO(po menuParameterPO) *biz.MenuParameter {
func menuParameterFromPO(po menuParameterPO) *system.MenuParameter {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.MenuParameter{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, MenuID: po.MenuID, Type: po.Type, Key: po.Key, Value: po.Value}
return &system.MenuParameter{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, MenuID: po.MenuID, Type: po.Type, Key: po.Key, Value: po.Value}
}
func menuButtonFromPO(po menuButtonPO) *biz.MenuButton {
func menuButtonFromPO(po menuButtonPO) *system.MenuButton {
var deletedAt *time.Time
if po.DeletedAt.Valid {
value := po.DeletedAt.Time
deletedAt = &value
}
return &biz.MenuButton{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID}
return &system.MenuButton{CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, DeletedAt: deletedAt, ID: po.ID, Name: po.Name, Description: po.Description, MenuID: po.MenuID}
}
func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) error {
@ -292,7 +291,7 @@ func (r *menuRepo) AuthorityMenuIDs(ctx context.Context, id uint) ([]uint, error
// menus. In particular, it deliberately does not call List: List applies the
// current request actor's strict-auth filter, which would incorrectly hide
// menus when an administrator inspects a different (child) authority.
func (r *menuRepo) ListAuthorityMenus(ctx context.Context, authorityID uint) ([]*biz.Menu, error) {
func (r *menuRepo) ListAuthorityMenus(ctx context.Context, authorityID uint) ([]*system.Menu, error) {
var pos []menuPO
db := r.data.DB().WithContext(ctx).Model(&menuPO{}).
Where("id IN (?)", r.data.DB().WithContext(ctx).Model(&authorityMenuPO{}).
@ -301,7 +300,7 @@ func (r *menuRepo) ListAuthorityMenus(ctx context.Context, authorityID uint) ([]
if err := db.Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.Menu, 0, len(pos))
out := make([]*system.Menu, 0, len(pos))
for _, po := range pos {
// This endpoint does not preload Parameters or MenuBtn. Keep
// both relations nil so the response shape remains compatible.

View File

@ -2,10 +2,10 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"time"
"kra/internal/biz"
"kra/internal/conf"
)
@ -31,7 +31,7 @@ func TestMenuCreatePreservesSubmittedTimestamps(t *testing.T) {
ctx := context.Background()
createdAt := time.Date(2025, time.January, 2, 3, 4, 5, 0, time.UTC)
updatedAt := time.Date(2025, time.February, 3, 4, 5, 6, 0, time.UTC)
menu := &biz.Menu{CreatedAt: createdAt, UpdatedAt: updatedAt, Name: "created", Path: "created", Component: "view/created.vue", Title: "Created"}
menu := &system.Menu{CreatedAt: createdAt, UpdatedAt: updatedAt, Name: "created", Path: "created", Component: "view/created.vue", Title: "Created"}
if err := (&menuRepo{data: data}).Create(ctx, menu); err != nil {
t.Fatal(err)
@ -90,16 +90,16 @@ func TestMenuUpdateRebuildsRelationsWithoutChangingRequestedIdentity(t *testing.
}
repo := &menuRepo{data: data}
err := repo.Update(ctx, &biz.Menu{
err := repo.Update(ctx, &system.Menu{
ID: 10,
Name: "after",
Path: "after",
Component: "view/after.vue",
Title: "After",
Parameters: []*biz.MenuParameter{{
Parameters: []*system.MenuParameter{{
ID: 21, CreatedAt: createdAt, UpdatedAt: updatedAt, Type: "params", Key: "new", Value: "value",
}},
Buttons: []*biz.MenuButton{{
Buttons: []*system.MenuButton{{
ID: 31, CreatedAt: createdAt, UpdatedAt: updatedAt, Name: "edit", Description: "new",
}},
})
@ -163,7 +163,7 @@ func TestSetMenuRolesStrictOnlyChangesManagedAuthorities(t *testing.T) {
}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &menuRepo{data: data}
if err := repo.SetMenuRoles(ctx, menu.ID, []uint{siblingID}); err == nil {
t.Fatal("SetMenuRoles() accepted an out-of-scope authority")

View File

@ -1,10 +1,9 @@
package system
import (
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
@ -98,4 +97,4 @@ func (menuParameterPO) TableName() string { return "sys_base_menu_parameters" }
type userRepo struct{ data Provider }
func NewUserRepo(data Provider) biz.UserRepo { return &userRepo{data: data} }
func NewUserRepo(data Provider) system.UserRepo { return &userRepo{data: data} }

View File

@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@ -30,13 +30,13 @@ type operationPO struct {
func (operationPO) TableName() string { return "sys_operation_records" }
func (r *auditRecorderRepo) RecordOperation(ctx context.Context, v *biz.OperationRecord) error {
func (r *auditRecorderRepo) RecordOperation(ctx context.Context, v *system.OperationRecord) error {
return r.data.DB().WithContext(ctx).Create(&operationPO{IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}).Error
}
func opFromPO(v operationPO) *biz.OperationRecord {
return &biz.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}
func opFromPO(v operationPO) *system.OperationRecord {
return &system.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}
}
func (r *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*biz.OperationRecord, int64, error) {
func (r *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *system.OperationRecord) ([]*system.OperationRecord, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&operationPO{})
if q != nil {
if q.Path != "" {
@ -58,7 +58,7 @@ func (r *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *
return nil, 0, err
}
users := auditUsers(ctx, r.data.DB().WithContext(ctx), operationUserIDs(pos))
out := make([]*biz.OperationRecord, 0, len(pos))
out := make([]*system.OperationRecord, 0, len(pos))
for _, po := range pos {
value := opFromPO(po)
value.User = users[po.UserID]
@ -77,8 +77,8 @@ func operationUserIDs(values []operationPO) []uint {
return ids
}
func auditUsers(ctx context.Context, db *gorm.DB, ids []uint) map[uint]*biz.User {
result := make(map[uint]*biz.User)
func auditUsers(ctx context.Context, db *gorm.DB, ids []uint) map[uint]*system.User {
result := make(map[uint]*system.User)
if len(ids) == 0 {
return result
}
@ -87,11 +87,11 @@ func auditUsers(ctx context.Context, db *gorm.DB, ids []uint) map[uint]*biz.User
return result
}
for _, value := range users {
result[value.ID] = &biz.User{ID: value.ID, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, UUID: value.UUID, Username: value.Username, NickName: value.NickName, HeaderImg: value.HeaderImg, AuthorityID: value.AuthorityID, DeptID: value.DeptID, Phone: value.Phone, Email: value.Email, Enable: value.Enable}
result[value.ID] = &system.User{ID: value.ID, CreatedAt: value.CreatedAt, UpdatedAt: value.UpdatedAt, UUID: value.UUID, Username: value.Username, NickName: value.NickName, HeaderImg: value.HeaderImg, AuthorityID: value.AuthorityID, DeptID: value.DeptID, Phone: value.Phone, Email: value.Email, Enable: value.Enable}
}
return result
}
func (r *auditQueryRepo) FindOperation(ctx context.Context, id uint) (*biz.OperationRecord, error) {
func (r *auditQueryRepo) FindOperation(ctx context.Context, id uint) (*system.OperationRecord, error) {
var po operationPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err

View File

@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/pkg/database/pagination"
"gorm.io/gorm"
@ -13,7 +13,7 @@ import (
type parameterRepo struct{ data Provider }
func NewParameterRepo(data Provider) biz.ParameterRepo { return &parameterRepo{data: data} }
func NewParameterRepo(data Provider) system.ParameterRepo { return &parameterRepo{data: data} }
type parameterPO struct {
ID uint `gorm:"primaryKey"`
@ -28,7 +28,7 @@ type parameterPO struct {
func (parameterPO) TableName() string { return "sys_params" }
func (r *parameterRepo) CreateParameter(ctx context.Context, v *biz.SystemParameter) error {
func (r *parameterRepo) CreateParameter(ctx context.Context, v *system.SystemParameter) error {
po := parameterPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@ -36,7 +36,7 @@ func (r *parameterRepo) CreateParameter(ctx context.Context, v *biz.SystemParame
v.ID, v.CreatedAt, v.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
return nil
}
func (r *parameterRepo) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error {
func (r *parameterRepo) UpdateParameter(ctx context.Context, v *system.SystemParameter) error {
// The compatible update uses a struct, so zero-value optional fields (notably desc)
// are intentionally ignored rather than clearing an existing value.
return r.data.DB().WithContext(ctx).Model(&parameterPO{}).Where("id = ?", v.ID).Updates(&parameterPO{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc}).Error
@ -47,21 +47,21 @@ func (r *parameterRepo) DeleteParameters(ctx context.Context, ids []string) erro
}
return r.data.DB().WithContext(ctx).Delete(&[]parameterPO{}, "id in ?", ids).Error
}
func (r *parameterRepo) FindParameterByID(ctx context.Context, id string) (*biz.SystemParameter, error) {
func (r *parameterRepo) FindParameterByID(ctx context.Context, id string) (*system.SystemParameter, error) {
var po parameterPO
if err := r.data.DB().WithContext(ctx).Where("id = ?", id).First(&po).Error; err != nil {
return nil, err
}
return parameterFromPO(po), nil
}
func (r *parameterRepo) FindParameterByKey(ctx context.Context, key string) (*biz.SystemParameter, error) {
func (r *parameterRepo) FindParameterByKey(ctx context.Context, key string) (*system.SystemParameter, error) {
var po parameterPO
if err := r.data.DB().WithContext(ctx).Where(parameterPO{Key: key}).First(&po).Error; err != nil {
return nil, err
}
return parameterFromPO(po), nil
}
func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *biz.SystemParameter) ([]*biz.SystemParameter, int64, error) {
func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *system.SystemParameter) ([]*system.SystemParameter, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&parameterPO{})
if q != nil {
if q.StartCreatedAt != nil && q.EndCreatedAt != nil {
@ -82,7 +82,7 @@ func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *b
if err := pagination.Apply(db, page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.SystemParameter, 0, len(pos))
out := make([]*system.SystemParameter, 0, len(pos))
for _, po := range pos {
out = append(out, parameterFromPO(po))
}

View File

@ -2,17 +2,16 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"time"
"kra/internal/biz"
)
func TestParameterRepositoryPreservesModelMetadata(t *testing.T) {
data := newTransactionTestData(t)
repo := &parameterRepo{data: data}
createdAt := time.Date(2025, time.January, 2, 3, 4, 5, 0, time.Local)
value := &biz.SystemParameter{ID: 41, CreatedAt: createdAt, Name: "name", Key: "key", Value: "value"}
value := &system.SystemParameter{ID: 41, CreatedAt: createdAt, Name: "name", Key: "key", Value: "value"}
if err := repo.CreateParameter(context.Background(), value); err != nil {
t.Fatal(err)
@ -40,7 +39,7 @@ func TestParameterRepositoryKeepsReferenceEmptyQuerySemantics(t *testing.T) {
data := newTransactionTestData(t)
repo := &parameterRepo{data: data}
ctx := context.Background()
first := &biz.SystemParameter{Name: "first", Key: "first", Value: "1"}
first := &system.SystemParameter{Name: "first", Key: "first", Value: "1"}
if err := repo.CreateParameter(ctx, first); err != nil {
t.Fatal(err)
}

View File

@ -3,16 +3,15 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
type permissionRepo struct{ data Provider }
func NewPermissionRepo(data Provider) biz.PermissionRepo { return &permissionRepo{data: data} }
func NewPermissionRepo(data Provider) system.PermissionRepo { return &permissionRepo{data: data} }
type menuButtonPO struct {
ID uint `gorm:"primaryKey"`
@ -34,12 +33,12 @@ type authorityButtonPO struct {
func (authorityButtonPO) TableName() string { return "sys_authority_btns" }
func (r *permissionRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuButton, error) {
func (r *permissionRepo) Buttons(ctx context.Context, menuID uint) ([]*system.MenuButton, error) {
var pos []menuButtonPO
if err := r.data.DB().WithContext(ctx).Where("sys_base_menu_id = ?", menuID).Find(&pos).Error; err != nil {
return nil, err
}
out := make([]*biz.MenuButton, 0, len(pos))
out := make([]*system.MenuButton, 0, len(pos))
for _, po := range pos {
out = append(out, menuButtonFromPO(po))
}

View File

@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
"kra/internal/conf"
)
@ -108,7 +108,7 @@ func TestSetSelectedButtonsStrictRequiresManagedRoleAndOwnedButton(t *testing.T)
if err := db.Create(&authorityButtonPO{AuthorityID: actorID, MenuID: 10, ButtonID: 31}).Error; err != nil {
t.Fatal(err)
}
ctx := biz.NewActorContext(context.Background(), biz.Actor{AuthorityID: actorID})
ctx := system.NewActorContext(context.Background(), system.Actor{AuthorityID: actorID})
repo := &permissionRepo{data: data}
if err := repo.SetSelectedButtons(ctx, siblingID, 10, []uint{31}); err == nil {
t.Fatal("SetSelectedButtons() accepted an out-of-scope authority")

View File

@ -3,16 +3,15 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
type positionRepo struct{ data Provider }
func NewPositionRepo(data Provider) biz.PositionRepo { return &positionRepo{data: data} }
func NewPositionRepo(data Provider) system.PositionRepo { return &positionRepo{data: data} }
type positionPO struct {
ID uint `gorm:"primaryKey"`
@ -37,13 +36,13 @@ type userPositionPO struct {
func (userPositionPO) TableName() string { return "sys_user_positions" }
func posFromPO(po positionPO) *biz.Position {
return &biz.Position{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Code: po.Code, Sort: po.Sort, Status: po.Status, Remark: po.Remark}
func posFromPO(po positionPO) *system.Position {
return &system.Position{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Code: po.Code, Sort: po.Sort, Status: po.Status, Remark: po.Remark}
}
func (r *positionRepo) CreatePosition(ctx context.Context, v *biz.Position) error {
func (r *positionRepo) CreatePosition(ctx context.Context, v *system.Position) error {
return r.data.DB().WithContext(ctx).Create(&positionPO{Name: v.Name, Code: v.Code, Sort: v.Sort, Status: v.Status, Remark: v.Remark}).Error
}
func (r *positionRepo) UpdatePosition(ctx context.Context, v *biz.Position) error {
func (r *positionRepo) UpdatePosition(ctx context.Context, v *system.Position) error {
return r.data.DB().WithContext(ctx).Model(&positionPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "code": v.Code, "sort": v.Sort, "status": v.Status, "remark": v.Remark}).Error
}
func (r *positionRepo) DeletePosition(ctx context.Context, id uint) error {
@ -59,14 +58,14 @@ func (r *positionRepo) DeletePosition(ctx context.Context, id uint) error {
}
return r.data.DB().WithContext(ctx).Delete(&positionPO{}, id).Error
}
func (r *positionRepo) FindPosition(ctx context.Context, id uint) (*biz.Position, error) {
func (r *positionRepo) FindPosition(ctx context.Context, id uint) (*system.Position, error) {
var po positionPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return posFromPO(po), nil
}
func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *biz.PositionListFilter) ([]*biz.Position, int64, error) {
func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *system.PositionListFilter) ([]*system.Position, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&positionPO{})
if q != nil {
if q.Name != "" {
@ -90,7 +89,7 @@ func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *biz
if err := db.Order("sort").Limit(size).Offset(size * (page - 1)).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.Position, 0, len(pos))
out := make([]*system.Position, 0, len(pos))
for _, po := range pos {
out = append(out, posFromPO(po))
}

View File

@ -4,9 +4,9 @@ package system
import (
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"kra/internal/conf"
"kra/internal/security"
@ -15,7 +15,7 @@ import (
type runtimeSettings struct{ runtime *conf.Runtime }
func NewRuntimeSettings(runtime *conf.Runtime) biz.RuntimeSettings {
func NewRuntimeSettings(runtime *conf.Runtime) system.RuntimeSettings {
return &runtimeSettings{runtime: runtime}
}
@ -27,8 +27,8 @@ func (s *runtimeSettings) RouterPrefix() string {
return config.RouterPrefix
}
func (s *runtimeSettings) JWTSettings() biz.JWTSettings {
value := biz.JWTSettings{Issuer: "kra", Expires: 7 * 24 * time.Hour, Buffer: 24 * time.Hour}
func (s *runtimeSettings) JWTSettings() system.JWTSettings {
value := system.JWTSettings{Issuer: "kra", Expires: 7 * 24 * time.Hour, Buffer: 24 * time.Hour}
config := s.runtime.Admin()
if config == nil || config.Jwt == nil {
return value
@ -46,8 +46,8 @@ func (s *runtimeSettings) JWTSettings() biz.JWTSettings {
return value
}
func (s *runtimeSettings) CaptchaSettings() biz.CaptchaSettings {
value := biz.CaptchaSettings{KeyLong: 6, ImageWidth: 240, ImageHeight: 80, StoreExpiration: 3 * time.Minute}
func (s *runtimeSettings) CaptchaSettings() system.CaptchaSettings {
value := system.CaptchaSettings{KeyLong: 6, ImageWidth: 240, ImageHeight: 80, StoreExpiration: 3 * time.Minute}
config := s.runtime.Admin()
if config == nil || config.Captcha == nil {
return value
@ -67,12 +67,12 @@ func (s *runtimeSettings) CaptchaSettings() biz.CaptchaSettings {
return value
}
func (s *runtimeSettings) MediaSettings() biz.MediaSettings {
func (s *runtimeSettings) MediaSettings() system.MediaSettings {
config := s.runtime.Admin()
if config == nil || config.Media == nil {
return biz.MediaSettings{}
return system.MediaSettings{}
}
return biz.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize, ChunkDir: config.Media.ChunkDir}
return system.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize, ChunkDir: config.Media.ChunkDir}
}
func (s *runtimeSettings) UseMultipoint() bool {
@ -80,13 +80,13 @@ func (s *runtimeSettings) UseMultipoint() bool {
return config != nil && config.System != nil && config.System.UseMultipoint
}
type tokenIssuer struct{ settings biz.RuntimeSettings }
type tokenIssuer struct{ settings system.RuntimeSettings }
func NewTokenIssuer(settings biz.RuntimeSettings) biz.TokenIssuer {
func NewTokenIssuer(settings system.RuntimeSettings) system.TokenIssuer {
return &tokenIssuer{settings: settings}
}
func (i *tokenIssuer) IssueToken(user *biz.User, authorityID uint, mustChangePassword bool, expires time.Duration) (*biz.IssuedToken, error) {
func (i *tokenIssuer) IssueToken(user *system.User, authorityID uint, mustChangePassword bool, expires time.Duration) (*system.IssuedToken, error) {
settings := i.settings.JWTSettings()
if expires <= 0 {
expires = settings.Expires
@ -99,10 +99,10 @@ func (i *tokenIssuer) IssueToken(user *biz.User, authorityID uint, mustChangePas
if err != nil {
return nil, err
}
return &biz.IssuedToken{Value: token, ExpiresAt: claims.ExpiresAt.Time, TTL: expires}, nil
return &system.IssuedToken{Value: token, ExpiresAt: claims.ExpiresAt.Time, TTL: expires}, nil
}
func (i *tokenIssuer) ReissueToken(source *biz.AuthClaims, authorityID uint) (*biz.IssuedToken, error) {
func (i *tokenIssuer) ReissueToken(source *system.AuthClaims, authorityID uint) (*system.IssuedToken, error) {
if source == nil {
return nil, errors.New("nil JWT claims")
}
@ -120,23 +120,23 @@ func (i *tokenIssuer) ReissueToken(source *biz.AuthClaims, authorityID uint) (*b
if err != nil {
return nil, err
}
return &biz.IssuedToken{Value: token, ExpiresAt: source.ExpiresAt, TTL: time.Until(source.ExpiresAt)}, nil
return &system.IssuedToken{Value: token, ExpiresAt: source.ExpiresAt, TTL: time.Until(source.ExpiresAt)}, nil
}
func (i *tokenIssuer) ParseToken(token string) (*biz.AuthClaims, error) {
func (i *tokenIssuer) ParseToken(token string) (*system.AuthClaims, error) {
claims, err := security.Parse(token, i.settings.JWTSettings().SigningKey)
if err != nil {
switch {
case errors.Is(err, security.ErrTokenExpired):
return nil, biz.ErrTokenExpired
return nil, system.ErrTokenExpired
case errors.Is(err, security.ErrTokenMalformed):
return nil, biz.ErrTokenMalformed
return nil, system.ErrTokenMalformed
case errors.Is(err, security.ErrTokenSignatureInvalid):
return nil, biz.ErrTokenSignatureInvalid
return nil, system.ErrTokenSignatureInvalid
case errors.Is(err, security.ErrTokenNotValidYet):
return nil, biz.ErrTokenNotValidYet
return nil, system.ErrTokenNotValidYet
default:
return nil, biz.ErrTokenInvalid
return nil, system.ErrTokenInvalid
}
}
audience := append([]string(nil), claims.Audience...)
@ -144,5 +144,5 @@ func (i *tokenIssuer) ParseToken(token string) (*biz.AuthClaims, error) {
if claims.IssuedAt != nil {
issuedAt = claims.IssuedAt.Time
}
return &biz.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, PasswordVersion: claims.PasswordVersion, Issuer: claims.Issuer, Audience: audience, IssuedAt: issuedAt, NotBefore: claims.NotBefore.Time, ExpiresAt: claims.ExpiresAt.Time}, nil
return &system.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, PasswordVersion: claims.PasswordVersion, Issuer: claims.Issuer, Audience: audience, IssuedAt: issuedAt, NotBefore: claims.NotBefore.Time, ExpiresAt: claims.ExpiresAt.Time}, nil
}

View File

@ -3,10 +3,9 @@ package system
import (
"context"
"errors"
"kra/internal/biz/system"
"time"
"kra/internal/biz"
"gorm.io/gorm"
)
@ -51,17 +50,17 @@ func DefaultSecurityConfig() SecurityConfigPO {
type securityRepo struct{ data DatabaseProvider }
func NewSecurityRepo(data DatabaseProvider) biz.SecurityRepo { return &securityRepo{data: data} }
func NewSecurityRepo(data DatabaseProvider) system.SecurityRepo { return &securityRepo{data: data} }
func securityFromPO(v SecurityConfigPO) *biz.SecurityConfig {
return &biz.SecurityConfig{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
func securityFromPO(v SecurityConfigPO) *system.SecurityConfig {
return &system.SecurityConfig{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
}
func securityToPO(v *biz.SecurityConfig) SecurityConfigPO {
func securityToPO(v *system.SecurityConfig) SecurityConfigPO {
return SecurityConfigPO{ID: 1, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword}
}
func (r *securityRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, error) {
func (r *securityRepo) SecurityConfig(ctx context.Context) (*system.SecurityConfig, error) {
if !r.data.DatabaseReady() {
po := DefaultSecurityConfig()
po.ID = 0
@ -80,7 +79,7 @@ func (r *securityRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig,
return securityFromPO(po), nil
}
func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error {
func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *system.SecurityConfig) error {
po := securityToPO(v)
if err := r.data.DB().WithContext(ctx).Save(&po).Error; err != nil {
return err

View File

@ -2,10 +2,10 @@ package system
import (
"context"
"kra/internal/biz/system"
"strings"
"time"
"kra/internal/biz"
platformmodule "kra/pkg/module"
"github.com/google/uuid"
@ -13,14 +13,14 @@ import (
"gorm.io/gorm"
)
func SeedSystem(ctx context.Context, db *gorm.DB, input *biz.DatabaseConfig, surfaces ...platformmodule.Surface) error {
func SeedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, surfaces ...platformmodule.Surface) error {
return seedSystem(ctx, db, input, nil, surfaces...)
}
// SeedSystemWithCatalog applies module-contributed administration surfaces and
// default timed tasks in one transaction. The system module remains the owner
// of the system tables, while other modules contribute through the catalog.
func SeedSystemWithCatalog(ctx context.Context, db *gorm.DB, input *biz.DatabaseConfig, catalog platformmodule.Catalog) error {
func SeedSystemWithCatalog(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, catalog platformmodule.Catalog) error {
surfaces := make([]platformmodule.Surface, 0, len(catalog.Definitions))
for _, definition := range catalog.Definitions {
surfaces = append(surfaces, definition.Surface)
@ -28,7 +28,7 @@ func SeedSystemWithCatalog(ctx context.Context, db *gorm.DB, input *biz.Database
return seedSystem(ctx, db, input, catalog.DefaultTimedTasks(), surfaces...)
}
func seedSystem(ctx context.Context, db *gorm.DB, input *biz.DatabaseConfig, defaults []platformmodule.TimedTask, surfaces ...platformmodule.Surface) error {
func seedSystem(ctx context.Context, db *gorm.DB, input *system.DatabaseConfig, defaults []platformmodule.TimedTask, surfaces ...platformmodule.Surface) error {
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
rootParentID := uint(0)
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", ParentID: &rootParentID, DataScope: 1, DefaultRouter: "dashboard"}

View File

@ -2,9 +2,9 @@ package system
import (
"context"
"kra/internal/biz/system"
"testing"
"kra/internal/biz"
platformmodule "kra/pkg/module"
)
@ -20,7 +20,7 @@ func TestSeedSystemCreatesInitialDataAndModuleSurface(t *testing.T) {
Menus: []platformmodule.Menu{{Name: "orders", Path: "orders", ParentName: "extensions", Component: "view/orders.vue", Title: "订单", Sort: 6}},
APIs: []platformmodule.API{{Path: "/orders", Method: "GET", Group: "订单", Description: "订单列表"}},
}
input := &biz.DatabaseConfig{AdminPassword: "admin-password", APIs: []*biz.API{{Path: "/healthz", Method: "GET", APIGroup: "系统"}}}
input := &system.DatabaseConfig{AdminPassword: "admin-password", APIs: []*system.API{{Path: "/healthz", Method: "GET", APIGroup: "系统"}}}
if err = SeedSystem(context.Background(), db, input, surface); err != nil {
t.Fatal(err)
}

View File

@ -2,7 +2,7 @@ package system
import (
"context"
"kra/internal/biz"
"kra/internal/biz/system"
"kra/pkg/database/gormkit"
"kra/pkg/database/pagination"
"time"
@ -46,14 +46,14 @@ func (taskLogPO) TableName() string { return "sys_timed_task_logs" }
type taskRepo struct{ data Provider }
func NewTaskRepo(data Provider) biz.TaskRepo { return &taskRepo{data: data} }
func taskToPO(v *biz.TimedTask) taskPO {
func NewTaskRepo(data Provider) system.TaskRepo { return &taskRepo{data: data} }
func taskToPO(v *system.TimedTask) taskPO {
return taskPO{ID: v.ID, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: gormkit.JSON(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: gormkit.JSON(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
}
func taskFromPO(v taskPO) *biz.TimedTask {
return &biz.TimedTask{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: []byte(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: []byte(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
func taskFromPO(v taskPO) *system.TimedTask {
return &system.TimedTask{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, Name: v.Name, Description: v.Description, Spec: v.Spec, WithSeconds: v.WithSeconds, ExecutorType: v.ExecutorType, MethodName: v.MethodName, Params: []byte(v.Params), HTTPURL: v.HTTPURL, HTTPMethod: v.HTTPMethod, HTTPHeader: []byte(v.HTTPHeader), HTTPBody: v.HTTPBody, HTTPAllowPrivate: v.HTTPAllowPrivate, Enabled: v.Enabled}
}
func (r *taskRepo) CreateTask(ctx context.Context, v *biz.TimedTask) error {
func (r *taskRepo) CreateTask(ctx context.Context, v *system.TimedTask) error {
po := taskToPO(v)
if err := r.data.DB().WithContext(ctx).Create(&po).Error; err != nil {
return err
@ -70,26 +70,26 @@ func (r *taskRepo) TaskNameExists(ctx context.Context, name string, excludeID ui
err := db.Count(&count).Error
return count > 0, err
}
func (r *taskRepo) UpdateTask(ctx context.Context, v *biz.TimedTask) error {
func (r *taskRepo) UpdateTask(ctx context.Context, v *system.TimedTask) error {
po := taskToPO(v)
return r.data.DB().WithContext(ctx).Model(&taskPO{}).Where("id = ?", v.ID).Select("name", "description", "spec", "with_seconds", "executor_type", "method_name", "params", "http_url", "http_method", "http_header", "http_body", "http_allow_private", "enabled").Updates(&po).Error
}
func (r *taskRepo) DeleteTask(ctx context.Context, id uint) error {
return r.data.DB().WithContext(ctx).Delete(&taskPO{}, id).Error
}
func (r *taskRepo) FindTask(ctx context.Context, id uint) (*biz.TimedTask, error) {
func (r *taskRepo) FindTask(ctx context.Context, id uint) (*system.TimedTask, error) {
var po taskPO
if err := r.data.DB().WithContext(ctx).First(&po, id).Error; err != nil {
return nil, err
}
return taskFromPO(po), nil
}
func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTask) ([]*biz.TimedTask, int64, error) {
func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *system.TimedTask) ([]*system.TimedTask, int64, error) {
// During first-install the data layer intentionally serves a bootstrap
// database without system tables. The scheduler starts before /init/initdb
// and should remain idle instead of logging a missing-table SQL error.
if !r.data.DatabaseReady() {
return []*biz.TimedTask{}, 0, nil
return []*system.TimedTask{}, 0, nil
}
db := r.data.DB().WithContext(ctx).Model(&taskPO{})
if q != nil {
@ -111,7 +111,7 @@ func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTa
if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.TimedTask, 0, len(pos))
out := make([]*system.TimedTask, 0, len(pos))
for _, po := range pos {
out = append(out, taskFromPO(po))
}
@ -120,13 +120,13 @@ func (r *taskRepo) ListTasks(ctx context.Context, page, size int, q *biz.TimedTa
func (r *taskRepo) ToggleTask(ctx context.Context, id uint, enabled bool) error {
return r.data.DB().WithContext(ctx).Model(&taskPO{}).Where("id = ?", id).Update("enabled", enabled).Error
}
func (r *taskRepo) RecordTaskLog(ctx context.Context, v *biz.TimedTaskLog) error {
func (r *taskRepo) RecordTaskLog(ctx context.Context, v *system.TimedTaskLog) error {
return r.data.DB().WithContext(ctx).Create(&taskLogPO{TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}).Error
}
func taskLogFromPO(v taskLogPO) *biz.TimedTaskLog {
return &biz.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
func taskLogFromPO(v taskLogPO) *system.TimedTaskLog {
return &system.TimedTaskLog{ID: v.ID, CreatedAt: v.CreatedAt, UpdatedAt: v.UpdatedAt, TaskID: v.TaskID, TaskName: v.TaskName, TriggerType: v.TriggerType, StartedAt: v.StartedAt, FinishedAt: v.FinishedAt, DurationMS: v.DurationMS, Status: v.Status, ErrorMsg: v.ErrorMsg, Output: v.Output}
}
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*biz.TimedTaskLog, int64, error) {
func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint, status string) ([]*system.TimedTaskLog, int64, error) {
db := r.data.DB().WithContext(ctx).Model(&taskLogPO{})
if taskID != 0 {
db = db.Where("task_id = ?", taskID)
@ -142,7 +142,7 @@ func (r *taskRepo) ListTaskLogs(ctx context.Context, page, size int, taskID uint
if err := pagination.Apply(db.Order("id desc"), page, size, 100).Find(&pos).Error; err != nil {
return nil, 0, err
}
out := make([]*biz.TimedTaskLog, 0, len(pos))
out := make([]*system.TimedTaskLog, 0, len(pos))
for _, po := range pos {
out = append(out, taskLogFromPO(po))
}

View File

@ -2,10 +2,10 @@ package system
import (
"context"
"kra/internal/biz/system"
"strings"
"testing"
"kra/internal/biz"
"kra/internal/conf"
)
@ -31,7 +31,7 @@ func TestUserAuthorityWritesAreAtomic(t *testing.T) {
t.Fatal(err)
}
repo := &userRepo{data: data}
user := &biz.User{Username: "atomic", Password: "hash", NickName: "before", AuthorityID: 888, Enable: 1}
user := &system.User{Username: "atomic", Password: "hash", NickName: "before", AuthorityID: 888, Enable: 1}
created, err := repo.CreateUserWithAuthorities(ctx, user, []uint{888, 999})
if err != nil {
t.Fatal(err)
@ -61,7 +61,7 @@ func TestUserAuthorityWritesAreAtomic(t *testing.T) {
t.Fatalf("missing-authority link count = %d, err = %v", missingAuthorityLinks, err)
}
withNewAuthority := &biz.User{Username: "with-new-authority", Password: "hash", AuthorityID: 888, Enable: 1}
withNewAuthority := &system.User{Username: "with-new-authority", Password: "hash", AuthorityID: 888, Enable: 1}
createdWithNewAuthority, err := repo.CreateUserWithAuthorities(ctx, withNewAuthority, []uint{888, 123456})
if err != nil {
t.Fatal(err)
@ -116,7 +116,7 @@ func TestUserRegistrationDoesNotAddPrimaryAuthorityToRequestedAssociations(t *te
if err := data.gormDB.WithContext(ctx).Create(&[]authorityPO{{AuthorityID: 888, AuthorityName: "primary"}, {AuthorityID: 999, AuthorityName: "requested"}}).Error; err != nil {
t.Fatal(err)
}
created, err := (&userRepo{data: data}).CreateUserWithAuthorities(ctx, &biz.User{Username: "association-shape", Password: "hash", AuthorityID: 888, Enable: 1}, []uint{999})
created, err := (&userRepo{data: data}).CreateUserWithAuthorities(ctx, &system.User{Username: "association-shape", Password: "hash", AuthorityID: 888, Enable: 1}, []uint{999})
if err != nil {
t.Fatal(err)
}
@ -173,8 +173,8 @@ func TestDictionaryImportKeepsHierarchyInOneTransaction(t *testing.T) {
repo := &dictionaryRepo{data: data}
parentID := uint(10)
active := true
dictionary := &biz.Dictionary{Name: "status", Type: "status", Status: &active}
details := []*biz.DictionaryDetail{{ID: 10, Label: "parent", Value: "1", Status: &active}, {ID: 11, Label: "child", Value: "2", ParentID: &parentID, Status: &active}}
dictionary := &system.Dictionary{Name: "status", Type: "status", Status: &active}
details := []*system.DictionaryDetail{{ID: 10, Label: "parent", Value: "1", Status: &active}, {ID: 11, Label: "child", Value: "2", ParentID: &parentID, Status: &active}}
if err := repo.ImportDictionary(ctx, dictionary, details); err != nil {
t.Fatal(err)
}

Some files were not shown because too many files have changed in this diff Show More