diff --git a/README.md b/README.md index 3991353..196f246 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ configs/ 运行配置 internal/biz/ 领域对象、用例和仓储接口(按 system/payment/integration/task 拆分) internal/data/ 数据库、缓存、对象存储及仓储实现 internal/server/ Gin 服务、路由、中间件和 Handler -internal/service/ HTTP 输入输出与领域对象转换 +internal/service/ 按业务模块组织的 HTTP 输入输出与领域对象转换 internal/worker/ 定时任务调度与后台工作进程 web/ Vue 管理端 ``` diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go index 0d56705..244a8c2 100644 --- a/cmd/wire_gen.go +++ b/cmd/wire_gen.go @@ -30,7 +30,10 @@ import ( "kra/internal/server" "kra/internal/server/handler" "kra/internal/server/router" - "kra/internal/service" + integration4 "kra/internal/service/integration" + payment3 "kra/internal/service/payment" + system3 "kra/internal/service/system" + task3 "kra/internal/service/task" "kra/internal/worker" "kra/pkg/logging" "log/slog" @@ -56,7 +59,7 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger authorityAccessRepo := system.NewAuthorityAccessRepo(dataData) apiRepo := system.NewAPIRepo(dataData) accessControlUsecase := system2.NewAccessControlUsecase(authorityAccessRepo, apiRepo) - accessControlService := service.NewAccessControlService(accessControlUsecase) + v := system3.NewAccessControlService(accessControlUsecase) userRepo := system.NewUserRepo(dataData) userUsecase := system2.NewUserUsecase(userRepo) securityRepo := system.NewSecurityRepo(dataData) @@ -68,45 +71,45 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger tokenIssuer := system.NewTokenIssuer(runtimeSettings) auditRecordRepo := system.NewAuditRecorderRepo(dataData) authenticationUsecase := system2.NewAuthenticationUsecase(userUsecase, securityUsecase, tokenIssuer, auditRecordRepo) - authService := service.NewAuthService(authenticationUsecase) - securityService := service.NewSecurityService(securityUsecase) + authService := system3.NewAuthService(authenticationUsecase) + v2 := system3.NewSecurityService(securityUsecase) auditRecorderUsecase := system2.NewAuditRecorderUsecase(auditRecordRepo) - auditRecorder := service.NewAuditRecorder(auditRecorderUsecase) + v3 := system3.NewAuditRecorder(auditRecorderUsecase) authorityUsecase := system2.NewAuthorityUsecase(authorityAccessRepo) - authorityService := service.NewAuthorityService(authorityUsecase) - authority := handler.NewAuthority(authorityService) + v4 := system3.NewAuthorityService(authorityUsecase) + authority := handler.NewAuthority(v4) menuRepo := system.NewMenuRepo(dataData) menuUsecase := system2.NewMenuUsecase(menuRepo) - menuService := service.NewMenuService(menuUsecase) - menu := handler.NewMenu(menuService) + v5 := system3.NewMenuService(menuUsecase) + menu := handler.NewMenu(v5) apiUsecase := system2.NewAPIUsecase(apiRepo) - apiService := service.NewAPIService(apiUsecase, runtimeSettings) - api := handler.NewAPI(apiService) + v6 := system3.NewAPIService(apiUsecase, runtimeSettings) + api := handler.NewAPI(v6) permissionRepo := system.NewPermissionRepo(dataData) permissionUsecase := system2.NewPermissionUsecase(permissionRepo) - permissionService := service.NewPermissionService(permissionUsecase) - permission := handler.NewPermission(permissionService) + v7 := system3.NewPermissionService(permissionUsecase) + permission := handler.NewPermission(v7) departmentRepo := system.NewDepartmentRepo(dataData) departmentUsecase := system2.NewDepartmentUsecase(departmentRepo) - departmentService := service.NewDepartmentService(departmentUsecase) + v8 := system3.NewDepartmentService(departmentUsecase) positionRepo := system.NewPositionRepo(dataData) positionUsecase := system2.NewPositionUsecase(positionRepo) - positionService := service.NewPositionService(positionUsecase) - organization := handler.NewOrganization(departmentService, positionService) + v9 := system3.NewPositionService(positionUsecase) + organization := handler.NewOrganization(v8, v9) announcementRepo := system.NewAnnouncementRepo(dataData) announcementUsecase := system2.NewAnnouncementUsecase(announcementRepo) - announcementService := service.NewAnnouncementService(announcementUsecase) - announcement := handler.NewAnnouncement(announcementService) + v10 := system3.NewAnnouncementService(announcementUsecase) + announcement := handler.NewAnnouncement(v10) emailRepo := email.NewEmailRepo(runtime) emailUsecase := system2.NewEmailUsecase(emailRepo) - emailService := service.NewEmailService(emailUsecase) - handlerEmail := handler.NewEmail(emailService) + v11 := system3.NewEmailService(emailUsecase) + handlerEmail := handler.NewEmail(v11) paymentConfigReader := integration.NewPaymentConfigReader(dataData) paymentRepo := payment.NewPaymentRepo(dataData, paymentConfigReader) paymentOrderRepo := payment.NewPaymentOrderRepo(dataData) paymentUsecase := payment2.NewPaymentUsecase(paymentRepo, paymentOrderRepo, logger) - paymentService := service.NewPaymentService(paymentUsecase) - handlerPayment := handler.NewPayment(paymentService) + v12 := payment3.NewPaymentService(paymentUsecase) + handlerPayment := handler.NewPayment(v12) taskRepo := task.NewTaskRepo(dataData) registry := app.TaskRegistry(catalog) taskUsecase := task2.NewTaskUsecaseWithRegistry(taskRepo, registry) @@ -116,53 +119,53 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger taskScheduler := worker.NewTaskScheduler(taskUsecase, authorityUsecase, taskExecutor, logger) taskRuntime := worker.NewTaskRuntime(taskScheduler) taskApplicationUsecase := task2.NewTaskApplicationUsecase(taskUsecase, taskRuntime) - taskService := service.NewTaskService(taskApplicationUsecase) - handlerTask := handler.NewTask(taskService) - mediaService := service.NewMediaService(mediaUsecase, runtimeSettings) - media := handler.NewMedia(mediaService) + v13 := task3.NewTaskService(taskApplicationUsecase) + handlerTask := handler.NewTask(v13) + v14 := system3.NewMediaService(mediaUsecase, runtimeSettings) + media := handler.NewMedia(v14) auditQueryRepo := system.NewAuditRepo(dataData) auditUsecase := system2.NewAuditUsecase(auditQueryRepo) - auditService := service.NewAuditService(auditUsecase) + v15 := system3.NewAuditService(auditUsecase) logFileRepo := system.NewLogFileRepo(dataData) logViewerUsecase := system2.NewLogViewerUsecase(logFileRepo) - logViewerService := service.NewLogViewerService(logViewerUsecase) - audit := handler.NewAudit(auditService, auditRecorder, logViewerService, logger) + v16 := system3.NewLogViewerService(logViewerUsecase) + audit := handler.NewAudit(v15, v3, v16, logger) exportRepo := system.NewExportRepo(dataData) exportUsecase := system2.NewExportUsecase(exportRepo) - exportService := service.NewExportService(exportUsecase, systemCache) - export := handler.NewExport(exportService) + v17 := system3.NewExportService(exportUsecase, systemCache) + export := handler.NewExport(v17) versionRepo := system.NewVersionRepo(dataData) versionUsecase := system2.NewVersionUsecase(versionRepo) - versionService := service.NewVersionService(versionUsecase) - version := handler.NewVersion(versionService) + v18 := system3.NewVersionService(versionUsecase) + version := handler.NewVersion(v18) dictionaryRepo := system.NewDictionaryRepo(dataData) dictionaryUsecase := system2.NewDictionaryUsecase(dictionaryRepo) - dictionaryService := service.NewDictionaryService(dictionaryUsecase) - dictionary := handler.NewDictionary(dictionaryService) + v19 := system3.NewDictionaryService(dictionaryUsecase) + dictionary := handler.NewDictionary(v19) parameterRepo := system.NewParameterRepo(dataData) parameterUsecase := system2.NewParameterUsecase(parameterRepo) - parameterService := service.NewParameterService(parameterUsecase) - parameter := handler.NewParameter(parameterService) - tokenService := service.NewTokenService(tokenUsecase, tokenIssuer) - apiToken := handler.NewAPIToken(tokenService) + v20 := system3.NewParameterService(parameterUsecase) + parameter := handler.NewParameter(v20) + v21 := system3.NewTokenService(tokenUsecase, tokenIssuer) + apiToken := handler.NewAPIToken(v21) initializationRepo := initialize.NewRepo(dataData, catalog) taskReloader := worker.NewTaskReloader(taskScheduler) systemConfigUsecase := system2.NewSystemConfigUsecase(initializationRepo, taskReloader) - systemConfigService := service.NewSystemConfigService(systemConfigUsecase, runtimeSettings) - systemConfig := handler.NewSystemConfig(systemConfigService, securityService) - public := handler.NewPublic(authService, systemConfigService, securityService) - userService := service.NewUserService(userUsecase, securityService) - user := handler.NewUser(userService, authService) - navigation := handler.NewNavigation(userService) - session := handler.NewSession(tokenService) + v22 := system3.NewSystemConfigService(systemConfigUsecase, runtimeSettings) + systemConfig := handler.NewSystemConfig(v22, v2) + public := handler.NewPublic(authService, v22, v2) + v23 := system3.NewUserService(userUsecase, v2) + user := handler.NewUser(v23, authService) + navigation := handler.NewNavigation(v23) + session := handler.NewSession(v21) integrationConfigRepo := integration.NewIntegrationConfigRepo(dataData) store := data.NewIntegrationRuntime(dataData) connectivityTester := integration2.NewConnectivityTester(store) integrationConfigUsecase := integration3.NewIntegrationConfigUsecase(integrationConfigRepo, connectivityTester) - integrationConfigService := service.NewIntegrationConfigService(integrationConfigUsecase) - integrationConfig := handler.NewIntegrationConfig(integrationConfigService) - v := handler.NewSet(authority, menu, api, permission, organization, announcement, handlerEmail, handlerPayment, handlerTask, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session, integrationConfig) - routes := router.NewRoutes(v) + v24 := integration4.NewIntegrationConfigService(integrationConfigUsecase) + integrationConfig := handler.NewIntegrationConfig(v24) + v25 := handler.NewSet(authority, menu, api, permission, organization, announcement, handlerEmail, handlerPayment, handlerTask, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session, integrationConfig) + routes := router.NewRoutes(v25) maintenanceRepo := system.NewMaintenanceRepo(dataData) maintenanceUsecase := system2.NewMaintenanceUsecase(maintenanceRepo) taskMethods := worker.NewTaskMethods(taskUsecase, maintenanceUsecase, mediaUsecase, runtime) @@ -173,7 +176,7 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger cleanup() return nil, nil, err } - engine := server.NewGinEngineWithRuntime(runtime, accessControlService, authService, securityService, auditRecorder, logger, string2, moduleRuntime, websocketServer) + engine := server.NewGinEngineWithRuntime(runtime, v, authService, v2, v3, logger, string2, moduleRuntime, websocketServer) httpServer := server.NewGinServer(confServer, engine) mqReloadable, cleanup3, err := mq.New(store, logger) if err != nil { @@ -181,7 +184,7 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger cleanup() return nil, nil, err } - kratosApp := newApp(logger, httpServer, taskScheduler, auditRecorder, reloadableLogger, mqReloadable) + kratosApp := newApp(logger, httpServer, taskScheduler, v3, reloadableLogger, mqReloadable) return kratosApp, func() { cleanup3() cleanup2() diff --git a/internal/README.md b/internal/README.md index a03ba63..ee59c12 100644 --- a/internal/README.md +++ b/internal/README.md @@ -16,9 +16,10 @@ - `routecatalog`:统一声明 HTTP 路由的公开性、操作审计、请求体策略和 API 元数据 - `security`:后台 JWT 等安全实现 - `server`:Gin server 组合与生命周期;横切 HTTP 代码按子包维护: - `server/handler`、`server/middleware`、`server/router`、`server/httpx` -- `service`:应用服务、DTO 与领域对象转换;DTO 集中在 - `service/dto`,根包中的 `dto_aliases.go` 只负责兼容旧调用方 + `server/handler`、`server/middleware`、`server/router`、`server/staticfiles`; + 通用响应和 Cookie 工具位于 `pkg/httpx` +- `service`:按 `system`、`payment`、`integration`、`task` 分模块的应用服务、 + DTO 与领域对象转换;DTO 集中在 `service/dto`,根包是兼容旧调用方的类型/构造器门面 - `worker`:定时任务执行与调度 目录代表边界,模块文件按资源命名。DTO、handler、中间件、路由和 HTTP diff --git a/internal/server/middleware/access.go b/internal/server/middleware/access.go index 1e2046c..20f1b7b 100644 --- a/internal/server/middleware/access.go +++ b/internal/server/middleware/access.go @@ -5,7 +5,7 @@ import ( "kra/internal/biz/system" "kra/internal/conf" - "kra/internal/service" + "kra/internal/utils/routepath" "github.com/gin-gonic/gin" ) @@ -30,7 +30,7 @@ func AccessControl(runtime *conf.Runtime, access accessController) gin.HandlerFu path := c.Request.URL.Path policyPath := path if config := runtime.Admin(); config != nil { - policyPath = service.NormalizeRoutePath(policyPath, config.RouterPrefix) + policyPath = routepath.Normalize(policyPath, config.RouterPrefix) } allowed, err := access.Authorize(c.Request.Context(), claims.AuthorityID, policyPath, c.Request.Method) if err != nil || !allowed { diff --git a/internal/service/README.md b/internal/service/README.md index fa3ed2d..7b550b9 100644 --- a/internal/service/README.md +++ b/internal/service/README.md @@ -2,8 +2,28 @@ `service` adapts HTTP-facing DTOs to business usecases and owns application orchestration. Request/response/filter contracts live in `dto/`; the root -package contains resource services, conversions, and security -coordination. +package is a compatibility facade and module provider aggregator. -`dto_alias.go` is a compatibility bridge for existing service callers. New -transport code should import `service/dto` directly when it needs a contract. +Implementations are grouped by the same business modules as `biz` and `data`: + +| Directory | Responsibility | +| --- | --- | +| `system/` | authentication, users, authorization, menus, organization, dictionaries, settings, audit, media, announcements, email, versions, exports | +| `payment/` | payment orchestration and callback handling | +| `integration/` | integration configuration orchestration | +| `task/` | scheduled task orchestration | +| `dto/` | HTTP request/response contracts | + +The stateless router-prefix helper lives in `internal/utils/routepath` because +both service modules and HTTP middleware use it; it does not depend on a +business usecase or DTO. + +Each module owns its Wire `ProviderSet`. The root `service` package re-exports +the existing service types and constructors through aliases and thin wrappers, so handlers, middleware +and generated Wire code can migrate independently without a flag day. + +Within `system/`, larger cross-cutting resources stay in the same package while +being split by concern (`audit.go` and its error/log companions, `media.go` and +upload companion, and `export.go`/`export_excel.go`). New transport code should +import the module package directly when it does not need the compatibility +facade. diff --git a/internal/service/integration_config.go b/internal/service/integration/integration_config.go similarity index 99% rename from internal/service/integration_config.go rename to internal/service/integration/integration_config.go index 993b103..aca5d6b 100644 --- a/internal/service/integration_config.go +++ b/internal/service/integration/integration_config.go @@ -1,4 +1,4 @@ -package service +package integration import ( "context" diff --git a/internal/service/integration/provider.go b/internal/service/integration/provider.go new file mode 100644 index 0000000..7d5bf13 --- /dev/null +++ b/internal/service/integration/provider.go @@ -0,0 +1,6 @@ +package integration + +import "github.com/google/wire" + +// ProviderSet wires integration transport services. +var ProviderSet = wire.NewSet(NewIntegrationConfigService) diff --git a/internal/service/payment.go b/internal/service/payment/payment.go similarity index 99% rename from internal/service/payment.go rename to internal/service/payment/payment.go index 99a5dfb..ffc63d7 100644 --- a/internal/service/payment.go +++ b/internal/service/payment/payment.go @@ -1,4 +1,4 @@ -package service +package payment import ( "context" diff --git a/internal/service/payment/provider.go b/internal/service/payment/provider.go new file mode 100644 index 0000000..6b2b755 --- /dev/null +++ b/internal/service/payment/provider.go @@ -0,0 +1,6 @@ +package payment + +import "github.com/google/wire" + +// ProviderSet wires payment transport services. +var ProviderSet = wire.NewSet(NewPaymentService) diff --git a/internal/service/service.go b/internal/service/service.go index 23840f4..4dac3d2 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -1,6 +1,148 @@ package service -import "github.com/google/wire" +// This package is a compatibility facade for the transport layer. Concrete +// implementations live under service/system, service/payment, +// service/integration, and service/task; aliases here preserve the original +// service.* API used by handlers and the composition root. -// ProviderSet is service providers. -var ProviderSet = wire.NewSet(NewAuthService, NewUserService, NewSystemConfigService, NewAuthorityService, NewPermissionService, NewAccessControlService, NewAPIService, NewMenuService, NewDepartmentService, NewPositionService, NewDictionaryService, NewParameterService, NewTokenService, NewSecurityService, NewVersionService, NewExportService, NewAuditService, NewAuditRecorder, NewLogViewerService, NewTaskService, NewMediaService, NewAnnouncementService, NewEmailService, NewPaymentService, NewIntegrationConfigService) +import ( + "kra/internal/biz/integration" + paymentbiz "kra/internal/biz/payment" + "kra/internal/biz/system" + taskbiz "kra/internal/biz/task" + "kra/internal/service/dto" + integrationservice "kra/internal/service/integration" + paymentservice "kra/internal/service/payment" + systemservice "kra/internal/service/system" + taskservice "kra/internal/service/task" + "kra/internal/utils/routepath" + + "github.com/google/wire" +) + +type AccessControlService = systemservice.AccessControlService +type AnnouncementInput = systemservice.AnnouncementInput +type AnnouncementService = systemservice.AnnouncementService +type APIService = systemservice.APIService +type AuditService = systemservice.AuditService +type AuditRecorder = systemservice.AuditRecorder +type AuthService = systemservice.AuthService +type AuthorityService = systemservice.AuthorityService +type DepartmentService = systemservice.DepartmentService +type DictionaryService = systemservice.DictionaryService +type EmailService = systemservice.EmailService +type ExportService = systemservice.ExportService +type ExportToken = systemservice.ExportToken +type LogViewerService = systemservice.LogViewerService +type MediaService = systemservice.MediaService +type MenuService = systemservice.MenuService +type ParameterService = systemservice.ParameterService +type PermissionService = systemservice.PermissionService +type PositionService = systemservice.PositionService +type SecurityService = systemservice.SecurityService +type PasswordPolicyError = systemservice.PasswordPolicyError +type SystemConfigService = systemservice.SystemConfigService +type TokenService = systemservice.TokenService +type VersionService = systemservice.VersionService +type PaymentService = paymentservice.PaymentService +type IntegrationConfigService = integrationservice.IntegrationConfigService +type TaskService = taskservice.TaskService +type UserService = systemservice.UserService + +func NewAccessControlService(uc *system.AccessControlUsecase) *AccessControlService { + return systemservice.NewAccessControlService(uc) +} +func NewAnnouncementService(uc *system.AnnouncementUsecase) *AnnouncementService { + return systemservice.NewAnnouncementService(uc) +} +func NewAPIService(uc *system.APIUsecase, settings system.RuntimeSettings) *APIService { + return systemservice.NewAPIService(uc, settings) +} +func NewAuditService(uc *system.AuditUsecase) *AuditService { + return systemservice.NewAuditService(uc) +} +func NewAuditRecorder(uc *system.AuditRecorderUsecase) *AuditRecorder { + return systemservice.NewAuditRecorder(uc) +} +func NewAuthService(uc *system.AuthenticationUsecase) *AuthService { + return systemservice.NewAuthService(uc) +} +func NewAuthorityService(uc *system.AuthorityUsecase) *AuthorityService { + return systemservice.NewAuthorityService(uc) +} +func NewDepartmentService(uc *system.DepartmentUsecase) *DepartmentService { + return systemservice.NewDepartmentService(uc) +} +func NewDictionaryService(uc *system.DictionaryUsecase) *DictionaryService { + return systemservice.NewDictionaryService(uc) +} +func NewEmailService(uc *system.EmailUsecase) *EmailService { + return systemservice.NewEmailService(uc) +} +func NewExportService(uc *system.ExportUsecase, cache system.Cache) *ExportService { + return systemservice.NewExportService(uc, cache) +} +func NewIntegrationConfigService(uc *integration.IntegrationConfigUsecase) *IntegrationConfigService { + return integrationservice.NewIntegrationConfigService(uc) +} +func NewLogViewerService(uc *system.LogViewerUsecase) *LogViewerService { + return systemservice.NewLogViewerService(uc) +} +func NewMediaService(uc *system.MediaUsecase, settings system.RuntimeSettings) *MediaService { + return systemservice.NewMediaService(uc, settings) +} +func NewMenuService(uc *system.MenuUsecase) *MenuService { + return systemservice.NewMenuService(uc) +} +func NewParameterService(uc *system.ParameterUsecase) *ParameterService { + return systemservice.NewParameterService(uc) +} +func NewPaymentService(uc *paymentbiz.PaymentUsecase) *PaymentService { + return paymentservice.NewPaymentService(uc) +} +func NewPermissionService(uc *system.PermissionUsecase) *PermissionService { + return systemservice.NewPermissionService(uc) +} +func NewPositionService(uc *system.PositionUsecase) *PositionService { + return systemservice.NewPositionService(uc) +} +func NewSecurityService(uc *system.SecurityUsecase) *SecurityService { + return systemservice.NewSecurityService(uc) +} +func NewSystemConfigService(uc *system.SystemConfigUsecase, settings system.RuntimeSettings) *SystemConfigService { + return systemservice.NewSystemConfigService(uc, settings) +} +func NewTaskService(uc *taskbiz.TaskApplicationUsecase) *TaskService { + return taskservice.NewTaskService(uc) +} +func NewTokenService(uc *system.TokenUsecase, issuer system.TokenIssuer) *TokenService { + return systemservice.NewTokenService(uc, issuer) +} +func NewUserService(uc *system.UserUsecase, settings *SecurityService) *UserService { + return systemservice.NewUserService(uc, settings) +} +func NewVersionService(uc *system.VersionUsecase) *VersionService { + return systemservice.NewVersionService(uc) +} + +var ErrExportTokenInvalid = systemservice.ErrExportTokenInvalid +var ErrExportTokenMalformed = systemservice.ErrExportTokenMalformed +var ErrExportTokenType = systemservice.ErrExportTokenType + +func NormalizeRoutePath(path, routerPrefix string) string { + return routepath.Normalize(path, routerPrefix) +} + +func IsPasswordPolicyError(err error) bool { return systemservice.IsPasswordPolicyError(err) } + +func DefaultPaymentCallbackAck(provider string, success bool) dto.PaymentCallbackAck { + return paymentservice.DefaultPaymentCallbackAck(provider, success) +} + +// ProviderSet aggregates all service modules for Wire. +var ProviderSet = wire.NewSet( + systemservice.ProviderSet, + paymentservice.ProviderSet, + integrationservice.ProviderSet, + taskservice.ProviderSet, +) diff --git a/internal/service/access_control.go b/internal/service/system/access_control.go similarity index 98% rename from internal/service/access_control.go rename to internal/service/system/access_control.go index c58fe6a..58bce76 100644 --- a/internal/service/access_control.go +++ b/internal/service/system/access_control.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/announcement.go b/internal/service/system/announcement.go similarity index 99% rename from internal/service/announcement.go rename to internal/service/system/announcement.go index 7a09175..3a4025e 100644 --- a/internal/service/announcement.go +++ b/internal/service/system/announcement.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/api.go b/internal/service/system/api.go similarity index 98% rename from internal/service/api.go rename to internal/service/system/api.go index f024c1f..9e5b0dd 100644 --- a/internal/service/api.go +++ b/internal/service/system/api.go @@ -1,8 +1,9 @@ -package service +package system import ( "context" "kra/internal/biz/system" + "kra/internal/utils/routepath" "strings" "kra/internal/service/dto" @@ -21,7 +22,7 @@ func (s *APIService) NormalizeRoutePath(path string) string { if s.settings == nil { return path } - return NormalizeRoutePath(path, s.settings.RouterPrefix()) + return routepath.Normalize(path, s.settings.RouterPrefix()) } func (s *APIService) apiDomain(value *dto.APIRequest) *system.API { diff --git a/internal/service/api_token.go b/internal/service/system/api_token.go similarity index 99% rename from internal/service/api_token.go rename to internal/service/system/api_token.go index 25fa2d9..74cda78 100644 --- a/internal/service/api_token.go +++ b/internal/service/system/api_token.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/audit.go b/internal/service/system/audit.go similarity index 99% rename from internal/service/audit.go rename to internal/service/system/audit.go index 17bc6eb..60d49ef 100644 --- a/internal/service/audit.go +++ b/internal/service/system/audit.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/audit_error.go b/internal/service/system/audit_error.go similarity index 99% rename from internal/service/audit_error.go rename to internal/service/system/audit_error.go index 26d87ef..409ab5b 100644 --- a/internal/service/audit_error.go +++ b/internal/service/system/audit_error.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/audit_log_file.go b/internal/service/system/audit_log_file.go similarity index 98% rename from internal/service/audit_log_file.go rename to internal/service/system/audit_log_file.go index 5f58e6c..1eaea0b 100644 --- a/internal/service/audit_log_file.go +++ b/internal/service/system/audit_log_file.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/authentication.go b/internal/service/system/authentication.go similarity index 98% rename from internal/service/authentication.go rename to internal/service/system/authentication.go index b971de3..4f293ec 100644 --- a/internal/service/authentication.go +++ b/internal/service/system/authentication.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/authority.go b/internal/service/system/authority.go similarity index 99% rename from internal/service/authority.go rename to internal/service/system/authority.go index 995c15f..319e120 100644 --- a/internal/service/authority.go +++ b/internal/service/system/authority.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/department.go b/internal/service/system/department.go similarity index 99% rename from internal/service/department.go rename to internal/service/system/department.go index a1ea3d6..474ff6f 100644 --- a/internal/service/department.go +++ b/internal/service/system/department.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/department_test.go b/internal/service/system/department_test.go similarity index 96% rename from internal/service/department_test.go rename to internal/service/system/department_test.go index 3a8fd4e..b2b1265 100644 --- a/internal/service/department_test.go +++ b/internal/service/system/department_test.go @@ -1,4 +1,4 @@ -package service +package system import ( "kra/internal/biz/system" diff --git a/internal/service/dictionary.go b/internal/service/system/dictionary.go similarity index 99% rename from internal/service/dictionary.go rename to internal/service/system/dictionary.go index 6efb5c3..3fe1aae 100644 --- a/internal/service/dictionary.go +++ b/internal/service/system/dictionary.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/dictionary_import.go b/internal/service/system/dictionary_import.go similarity index 99% rename from internal/service/dictionary_import.go rename to internal/service/system/dictionary_import.go index 674a179..130626d 100644 --- a/internal/service/dictionary_import.go +++ b/internal/service/system/dictionary_import.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/dictionary_test.go b/internal/service/system/dictionary_test.go similarity index 97% rename from internal/service/dictionary_test.go rename to internal/service/system/dictionary_test.go index d2263de..ec5401e 100644 --- a/internal/service/dictionary_test.go +++ b/internal/service/system/dictionary_test.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/email.go b/internal/service/system/email.go similarity index 97% rename from internal/service/email.go rename to internal/service/system/email.go index 4ea578b..a2b871a 100644 --- a/internal/service/email.go +++ b/internal/service/system/email.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/export.go b/internal/service/system/export.go similarity index 99% rename from internal/service/export.go rename to internal/service/system/export.go index e4d9556..30dfeb2 100644 --- a/internal/service/export.go +++ b/internal/service/system/export.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/export_excel.go b/internal/service/system/export_excel.go similarity index 99% rename from internal/service/export_excel.go rename to internal/service/system/export_excel.go index 09ab693..c8a70fa 100644 --- a/internal/service/export_excel.go +++ b/internal/service/system/export_excel.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/media.go b/internal/service/system/media.go similarity index 99% rename from internal/service/media.go rename to internal/service/system/media.go index 1362a75..c507a0e 100644 --- a/internal/service/media.go +++ b/internal/service/system/media.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/media_upload.go b/internal/service/system/media_upload.go similarity index 98% rename from internal/service/media_upload.go rename to internal/service/system/media_upload.go index edae30d..527b885 100644 --- a/internal/service/media_upload.go +++ b/internal/service/system/media_upload.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/menu.go b/internal/service/system/menu.go similarity index 99% rename from internal/service/menu.go rename to internal/service/system/menu.go index 4574cf6..3e3106d 100644 --- a/internal/service/menu.go +++ b/internal/service/system/menu.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/menu_test.go b/internal/service/system/menu_test.go similarity index 99% rename from internal/service/menu_test.go rename to internal/service/system/menu_test.go index 510b933..db72e45 100644 --- a/internal/service/menu_test.go +++ b/internal/service/system/menu_test.go @@ -1,4 +1,4 @@ -package service +package system import ( "encoding/json" diff --git a/internal/service/parameter.go b/internal/service/system/parameter.go similarity index 99% rename from internal/service/parameter.go rename to internal/service/system/parameter.go index cc50a53..b1f9233 100644 --- a/internal/service/parameter.go +++ b/internal/service/system/parameter.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/permission.go b/internal/service/system/permission.go similarity index 97% rename from internal/service/permission.go rename to internal/service/system/permission.go index fe3f79c..a79eca1 100644 --- a/internal/service/permission.go +++ b/internal/service/system/permission.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/position.go b/internal/service/system/position.go similarity index 99% rename from internal/service/position.go rename to internal/service/system/position.go index 19f6d20..ba2767a 100644 --- a/internal/service/position.go +++ b/internal/service/system/position.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/system/provider.go b/internal/service/system/provider.go new file mode 100644 index 0000000..3b2bc56 --- /dev/null +++ b/internal/service/system/provider.go @@ -0,0 +1,30 @@ +package system + +import "github.com/google/wire" + +// ProviderSet wires HTTP-facing services backed by the system business +// module. +var ProviderSet = wire.NewSet( + NewAuthService, + NewUserService, + NewSystemConfigService, + NewAuthorityService, + NewPermissionService, + NewAccessControlService, + NewAPIService, + NewMenuService, + NewDepartmentService, + NewPositionService, + NewDictionaryService, + NewParameterService, + NewTokenService, + NewSecurityService, + NewVersionService, + NewExportService, + NewAuditService, + NewAuditRecorder, + NewLogViewerService, + NewMediaService, + NewAnnouncementService, + NewEmailService, +) diff --git a/internal/service/security.go b/internal/service/system/security.go similarity index 99% rename from internal/service/security.go rename to internal/service/system/security.go index cf9e38e..861ffa6 100644 --- a/internal/service/security.go +++ b/internal/service/system/security.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/security_session.go b/internal/service/system/security_session.go similarity index 99% rename from internal/service/security_session.go rename to internal/service/system/security_session.go index 836dfa8..bb16872 100644 --- a/internal/service/security_session.go +++ b/internal/service/system/security_session.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/system.go b/internal/service/system/system.go similarity index 96% rename from internal/service/system.go rename to internal/service/system/system.go index 19286e5..8668a0f 100644 --- a/internal/service/system.go +++ b/internal/service/system/system.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/system_config.go b/internal/service/system/system_config.go similarity index 97% rename from internal/service/system_config.go rename to internal/service/system/system_config.go index e1806e9..d5ce929 100644 --- a/internal/service/system_config.go +++ b/internal/service/system/system_config.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/system_info.go b/internal/service/system/system_info.go similarity index 98% rename from internal/service/system_info.go rename to internal/service/system/system_info.go index 8cc810e..27a1c8b 100644 --- a/internal/service/system_info.go +++ b/internal/service/system/system_info.go @@ -1,4 +1,4 @@ -package service +package system import ( "runtime" diff --git a/internal/service/system_init.go b/internal/service/system/system_init.go similarity index 92% rename from internal/service/system_init.go rename to internal/service/system/system_init.go index 58006e8..b839273 100644 --- a/internal/service/system_init.go +++ b/internal/service/system/system_init.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" @@ -6,6 +6,7 @@ import ( "kra/internal/biz/system" "kra/internal/routecatalog" "kra/internal/service/dto" + "kra/internal/utils/routepath" ) func (s *SystemConfigService) Initialize(ctx context.Context, input *dto.DatabaseInitRequest, apis []*system.API) error { @@ -28,7 +29,7 @@ func (s *SystemConfigService) Initialize(ctx context.Context, input *dto.Databas func (s *SystemConfigService) InitializeRoutes(ctx context.Context, input *dto.DatabaseInitRequest, routes []dto.Route) error { apis := make([]*system.API, 0, len(routes)) for _, route := range routes { - path := NormalizeRoutePath(route.Path, s.settings.RouterPrefix()) + path := routepath.Normalize(route.Path, s.settings.RouterPrefix()) descriptor := routecatalog.Describe(route.Method, path) group, description := descriptor.Group, descriptor.Description apis = append(apis, &system.API{Path: path, Method: route.Method, APIGroup: group, Description: description}) diff --git a/internal/service/user.go b/internal/service/system/user.go similarity index 99% rename from internal/service/user.go rename to internal/service/system/user.go index 45e0977..85e12ea 100644 --- a/internal/service/user.go +++ b/internal/service/system/user.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/user_conversion.go b/internal/service/system/user_conversion.go similarity index 99% rename from internal/service/user_conversion.go rename to internal/service/system/user_conversion.go index 7c002a0..5c01d46 100644 --- a/internal/service/user_conversion.go +++ b/internal/service/system/user_conversion.go @@ -1,4 +1,4 @@ -package service +package system import ( "kra/internal/biz/system" diff --git a/internal/service/version.go b/internal/service/system/version.go similarity index 99% rename from internal/service/version.go rename to internal/service/system/version.go index 48f6483..a8bb461 100644 --- a/internal/service/version.go +++ b/internal/service/system/version.go @@ -1,4 +1,4 @@ -package service +package system import ( "context" diff --git a/internal/service/task/provider.go b/internal/service/task/provider.go new file mode 100644 index 0000000..0d08056 --- /dev/null +++ b/internal/service/task/provider.go @@ -0,0 +1,6 @@ +package task + +import "github.com/google/wire" + +// ProviderSet wires task transport services. +var ProviderSet = wire.NewSet(NewTaskService) diff --git a/internal/service/task.go b/internal/service/task/task.go similarity index 99% rename from internal/service/task.go rename to internal/service/task/task.go index 508f2fd..a589447 100644 --- a/internal/service/task.go +++ b/internal/service/task/task.go @@ -1,4 +1,4 @@ -package service +package task import ( "context" diff --git a/internal/service/route_path.go b/internal/utils/routepath/routepath.go similarity index 69% rename from internal/service/route_path.go rename to internal/utils/routepath/routepath.go index 600dd1c..9fe0813 100644 --- a/internal/service/route_path.go +++ b/internal/utils/routepath/routepath.go @@ -1,10 +1,10 @@ -package service +package routepath import "strings" -// NormalizeRoutePath removes only a complete configured router prefix. Paths +// Normalize removes only a complete configured router prefix. Paths // such as /administrator must not be shortened when the prefix is /admin. -func NormalizeRoutePath(path, routerPrefix string) string { +func Normalize(path, routerPrefix string) string { prefix := strings.TrimSuffix(strings.TrimSpace(routerPrefix), "/") if prefix == "" || prefix == "/" { return path diff --git a/internal/service/route_path_test.go b/internal/utils/routepath/routepath_test.go similarity index 77% rename from internal/service/route_path_test.go rename to internal/utils/routepath/routepath_test.go index 464183c..69eeb3d 100644 --- a/internal/service/route_path_test.go +++ b/internal/utils/routepath/routepath_test.go @@ -1,8 +1,8 @@ -package service +package routepath import "testing" -func TestNormalizeRoutePath(t *testing.T) { +func TestNormalize(t *testing.T) { tests := []struct { name string path string @@ -18,8 +18,8 @@ func TestNormalizeRoutePath(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := NormalizeRoutePath(tt.path, tt.prefix); got != tt.want { - t.Fatalf("NormalizeRoutePath(%q, %q) = %q, want %q", tt.path, tt.prefix, got, tt.want) + if got := Normalize(tt.path, tt.prefix); got != tt.want { + t.Fatalf("Normalize(%q, %q) = %q, want %q", tt.path, tt.prefix, got, tt.want) } }) } diff --git a/internal/worker/task_runtime_test.go b/internal/worker/task_runtime_test.go index 0029763..55b8891 100644 --- a/internal/worker/task_runtime_test.go +++ b/internal/worker/task_runtime_test.go @@ -272,3 +272,39 @@ func TestStopCancelsAndWaitsForActiveRuns(t *testing.T) { t.Fatal("scheduler accepted a new run after Stop()") } } + +func TestStartReturnsAfterStop(t *testing.T) { + scheduler := newTestTaskScheduler(&workerTaskRepo{}) + started := make(chan error, 1) + go func() { + started <- scheduler.Start(context.Background()) + }() + + deadline := time.Now().Add(time.Second) + for { + scheduler.ctxMu.RLock() + ready := scheduler.cancel != nil + scheduler.ctxMu.RUnlock() + if ready { + break + } + if time.Now().After(deadline) { + t.Fatal("scheduler did not start") + } + time.Sleep(time.Millisecond) + } + + stopCtx, stopCancel := context.WithTimeout(context.Background(), time.Second) + defer stopCancel() + if err := scheduler.Stop(stopCtx); err != nil { + t.Fatalf("Stop() error = %v", err) + } + select { + case err := <-started: + if err != nil { + t.Fatalf("Start() error = %v", err) + } + case <-time.After(time.Second): + t.Fatal("Start() did not return after Stop()") + } +} diff --git a/internal/worker/task_scheduler.go b/internal/worker/task_scheduler.go index 73e5064..1c15a5b 100644 --- a/internal/worker/task_scheduler.go +++ b/internal/worker/task_scheduler.go @@ -40,6 +40,9 @@ type scheduledEntry struct { } func NewTaskScheduler(tasks *taskbiz.TaskUsecase, authorities *system.AuthorityUsecase, executor *TaskExecutor, logger *slog.Logger) *TaskScheduler { + if logger == nil { + logger = slog.Default() + } idle := make(chan struct{}) close(idle) return &TaskScheduler{tasks: tasks, authorities: authorities, executor: executor, logger: logger.With("mod", "timedTask"), standard: cron.New(), seconds: cron.New(cron.WithSeconds()), entries: map[uint]scheduledEntry{}, running: map[uint]struct{}{}, idle: idle, subscribers: map[uint]map[chan []byte]struct{}{}} @@ -50,24 +53,42 @@ func NewTaskRuntime(scheduler *TaskScheduler) taskbiz.TaskRuntime { return sched func NewTaskReloader(scheduler *TaskScheduler) system.TaskReloader { return scheduler } func (s *TaskScheduler) Start(ctx context.Context) error { + if ctx == nil { + ctx = context.Background() + } runContext, cancel := context.WithCancel(ctx) s.ctxMu.Lock() s.runContext, s.cancel = runContext, cancel s.ctxMu.Unlock() s.runMu.Lock() + if s.stopping { + s.runMu.Unlock() + cancel() + return nil + } s.stopping = false - s.runMu.Unlock() s.standard.Start() s.seconds.Start() - if err := s.Reload(ctx); err != nil { - s.logger.WarnContext(ctx, "timed task table is not ready", "error", err) + if err := s.Reload(runContext); err != nil { + s.logger.WarnContext(runContext, "timed task table is not ready", "error", err) + } + s.runMu.Unlock() + // Stop cancels runContext. Waiting on the child context is important: the + // application lifecycle passes a long-lived parent context to Start and + // invokes Stop separately during graceful shutdown. + <-runContext.Done() + if ctx.Err() != nil { + cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cleanupCancel() + _ = s.Stop(cleanupCtx) } - <-ctx.Done() return nil } func (s *TaskScheduler) Stop(ctx context.Context) error { - s.closeSubscribers() + if ctx == nil { + ctx = context.Background() + } s.runMu.Lock() s.stopping = true idle := s.idle @@ -75,6 +96,16 @@ func (s *TaskScheduler) Stop(ctx context.Context) error { idle = make(chan struct{}) close(idle) } + // Keep the lifecycle lock while closing subscriber channels so a new SSE + // subscription cannot arrive between the shutdown flag and the close. + s.subMu.Lock() + for userID, subscribers := range s.subscribers { + for ch := range subscribers { + close(ch) + } + delete(s.subscribers, userID) + } + s.subMu.Unlock() s.runMu.Unlock() s.ctxMu.Lock() cancel := s.cancel @@ -329,6 +360,15 @@ func (s *TaskScheduler) Trigger(task *taskbiz.TimedTask) bool { func (s *TaskScheduler) Subscribe(userID uint) chan []byte { ch := make(chan []byte, 16) + s.runMu.Lock() + if s.stopping { + close(ch) + s.runMu.Unlock() + return ch + } + if s.subscribers == nil { + s.subscribers = make(map[uint]map[chan []byte]struct{}) + } s.subMu.Lock() if s.subscribers[userID] == nil { s.subscribers[userID] = map[chan []byte]struct{}{} @@ -342,6 +382,7 @@ func (s *TaskScheduler) Subscribe(userID uint) chan []byte { } s.subscribers[userID][ch] = struct{}{} s.subMu.Unlock() + s.runMu.Unlock() return ch } func (s *TaskScheduler) Unsubscribe(userID uint, ch chan []byte) {