优化结构

This commit is contained in:
Yvan
2026-08-23 02:36:58 +08:00
parent 7d89e2ea4d
commit 40632c5756
50 changed files with 408 additions and 115 deletions
+4 -3
View File
@@ -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
+2 -2
View File
@@ -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 {
+24 -4
View File
@@ -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.
@@ -1,4 +1,4 @@
package service
package integration
import (
"context"
+6
View File
@@ -0,0 +1,6 @@
package integration
import "github.com/google/wire"
// ProviderSet wires integration transport services.
var ProviderSet = wire.NewSet(NewIntegrationConfigService)
@@ -1,4 +1,4 @@
package service
package payment
import (
"context"
+6
View File
@@ -0,0 +1,6 @@
package payment
import "github.com/google/wire"
// ProviderSet wires payment transport services.
var ProviderSet = wire.NewSet(NewPaymentService)
+145 -3
View File
@@ -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,
)
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -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 {
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"kra/internal/biz/system"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"encoding/json"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
+30
View File
@@ -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,
)
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"runtime"
@@ -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})
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
@@ -1,4 +1,4 @@
package service
package system
import (
"kra/internal/biz/system"
@@ -1,4 +1,4 @@
package service
package system
import (
"context"
+6
View File
@@ -0,0 +1,6 @@
package task
import "github.com/google/wire"
// ProviderSet wires task transport services.
var ProviderSet = wire.NewSet(NewTaskService)
@@ -1,4 +1,4 @@
package service
package task
import (
"context"
@@ -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
@@ -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)
}
})
}
+36
View File
@@ -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()")
}
}
+46 -5
View File
@@ -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) {