优化结构
This commit is contained in:
parent
b88c220e20
commit
01b5c2f78a
File diff suppressed because it is too large
Load Diff
|
|
@ -11,6 +11,11 @@ import (
|
||||||
"kra/internal/biz"
|
"kra/internal/biz"
|
||||||
"kra/internal/conf"
|
"kra/internal/conf"
|
||||||
"kra/internal/data"
|
"kra/internal/data"
|
||||||
|
"kra/internal/data/payment"
|
||||||
|
"kra/internal/data/system"
|
||||||
|
"kra/internal/integration/cache"
|
||||||
|
"kra/internal/integration/email"
|
||||||
|
"kra/internal/integration/storage"
|
||||||
"kra/internal/server"
|
"kra/internal/server"
|
||||||
"kra/internal/server/handler"
|
"kra/internal/server/handler"
|
||||||
"kra/internal/service"
|
"kra/internal/service"
|
||||||
|
|
@ -27,58 +32,57 @@ import (
|
||||||
|
|
||||||
// wireApp init kratos application.
|
// wireApp init kratos application.
|
||||||
func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger, reloadableLogger *logging.ReloadableLogger, string2 string) (*kratos.App, func(), error) {
|
func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger, reloadableLogger *logging.ReloadableLogger, string2 string) (*kratos.App, func(), error) {
|
||||||
dataData, cleanup, err := data.NewData(runtime, logger)
|
reloadable, err := storage.NewFileStorage(runtime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
authorityAccessRepo := data.NewAuthorityAccessRepo(dataData)
|
dataData, cleanup, err := data.NewData(runtime, logger, reloadable)
|
||||||
apiRepo := data.NewAPIRepo(dataData)
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
authorityAccessRepo := system.NewAuthorityAccessRepo(dataData)
|
||||||
|
apiRepo := system.NewAPIRepo(dataData)
|
||||||
accessControlUsecase := biz.NewAccessControlUsecase(authorityAccessRepo, apiRepo)
|
accessControlUsecase := biz.NewAccessControlUsecase(authorityAccessRepo, apiRepo)
|
||||||
accessControlService := service.NewAccessControlService(accessControlUsecase)
|
accessControlService := service.NewAccessControlService(accessControlUsecase)
|
||||||
authorityUsecase := biz.NewAuthorityUsecase(authorityAccessRepo)
|
authorityUsecase := biz.NewAuthorityUsecase(authorityAccessRepo)
|
||||||
authorityService := service.NewAuthorityService(authorityUsecase)
|
authorityService := service.NewAuthorityService(authorityUsecase)
|
||||||
authority := handler.NewAuthority(authorityService)
|
authority := handler.NewAuthority(authorityService)
|
||||||
menuRepo := data.NewMenuRepo(dataData)
|
menuRepo := system.NewMenuRepo(dataData)
|
||||||
menuUsecase := biz.NewMenuUsecase(menuRepo)
|
menuUsecase := biz.NewMenuUsecase(menuRepo)
|
||||||
menuService := service.NewMenuService(menuUsecase)
|
menuService := service.NewMenuService(menuUsecase)
|
||||||
menu := handler.NewMenu(menuService)
|
menu := handler.NewMenu(menuService)
|
||||||
apiUsecase := biz.NewAPIUsecase(apiRepo)
|
apiUsecase := biz.NewAPIUsecase(apiRepo)
|
||||||
runtimeSettings := data.NewRuntimeSettings(runtime)
|
runtimeSettings := system.NewRuntimeSettings(runtime)
|
||||||
apiService := service.NewAPIService(apiUsecase, runtimeSettings)
|
apiService := service.NewAPIService(apiUsecase, runtimeSettings)
|
||||||
api := handler.NewAPI(apiService)
|
api := handler.NewAPI(apiService)
|
||||||
permissionRepo := data.NewPermissionRepo(dataData)
|
permissionRepo := system.NewPermissionRepo(dataData)
|
||||||
permissionUsecase := biz.NewPermissionUsecase(permissionRepo)
|
permissionUsecase := biz.NewPermissionUsecase(permissionRepo)
|
||||||
permissionService := service.NewPermissionService(permissionUsecase)
|
permissionService := service.NewPermissionService(permissionUsecase)
|
||||||
permission := handler.NewPermission(permissionService)
|
permission := handler.NewPermission(permissionService)
|
||||||
departmentRepo := data.NewDepartmentRepo(dataData)
|
departmentRepo := system.NewDepartmentRepo(dataData)
|
||||||
departmentUsecase := biz.NewDepartmentUsecase(departmentRepo)
|
departmentUsecase := biz.NewDepartmentUsecase(departmentRepo)
|
||||||
departmentService := service.NewDepartmentService(departmentUsecase)
|
departmentService := service.NewDepartmentService(departmentUsecase)
|
||||||
positionRepo := data.NewPositionRepo(dataData)
|
positionRepo := system.NewPositionRepo(dataData)
|
||||||
positionUsecase := biz.NewPositionUsecase(positionRepo)
|
positionUsecase := biz.NewPositionUsecase(positionRepo)
|
||||||
positionService := service.NewPositionService(positionUsecase)
|
positionService := service.NewPositionService(positionUsecase)
|
||||||
organization := handler.NewOrganization(departmentService, positionService)
|
organization := handler.NewOrganization(departmentService, positionService)
|
||||||
announcementRepo := data.NewAnnouncementRepo(dataData)
|
announcementRepo := system.NewAnnouncementRepo(dataData)
|
||||||
announcementUsecase := biz.NewAnnouncementUsecase(announcementRepo)
|
announcementUsecase := biz.NewAnnouncementUsecase(announcementRepo)
|
||||||
announcementService := service.NewAnnouncementService(announcementUsecase)
|
announcementService := service.NewAnnouncementService(announcementUsecase)
|
||||||
announcement := handler.NewAnnouncement(announcementService)
|
announcement := handler.NewAnnouncement(announcementService)
|
||||||
emailRepo := data.NewEmailRepo(runtime)
|
emailRepo := email.NewEmailRepo(runtime)
|
||||||
emailUsecase := biz.NewEmailUsecase(emailRepo)
|
emailUsecase := biz.NewEmailUsecase(emailRepo)
|
||||||
emailService := service.NewEmailService(emailUsecase)
|
emailService := service.NewEmailService(emailUsecase)
|
||||||
email := handler.NewEmail(emailService)
|
handlerEmail := handler.NewEmail(emailService)
|
||||||
paymentRepo := data.NewPaymentRepo(dataData)
|
paymentRepo := payment.NewPaymentRepo(dataData)
|
||||||
paymentOrderRepo := data.NewPaymentOrderRepo(dataData)
|
paymentOrderRepo := payment.NewPaymentOrderRepo(dataData)
|
||||||
paymentUsecase := biz.NewPaymentUsecase(paymentRepo, paymentOrderRepo, logger)
|
paymentUsecase := biz.NewPaymentUsecase(paymentRepo, paymentOrderRepo, logger)
|
||||||
paymentService := service.NewPaymentService(paymentUsecase)
|
paymentService := service.NewPaymentService(paymentUsecase)
|
||||||
payment := handler.NewPayment(paymentService)
|
handlerPayment := handler.NewPayment(paymentService)
|
||||||
taskRepo := data.NewTaskRepo(dataData)
|
taskRepo := system.NewTaskRepo(dataData)
|
||||||
taskUsecase := biz.NewTaskUsecase(taskRepo)
|
taskUsecase := biz.NewTaskUsecase(taskRepo)
|
||||||
mediaRepo := data.NewMediaRepo(dataData)
|
mediaRepo := system.NewMediaRepo(dataData)
|
||||||
fileStorage, err := data.NewFileStorage(dataData)
|
mediaUsecase := biz.NewMediaUsecase(mediaRepo, reloadable, runtimeSettings)
|
||||||
if err != nil {
|
|
||||||
cleanup()
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
mediaUsecase := biz.NewMediaUsecase(mediaRepo, fileStorage, runtimeSettings)
|
|
||||||
taskExecutor := worker.NewTaskExecutor(taskUsecase, mediaUsecase, runtime)
|
taskExecutor := worker.NewTaskExecutor(taskUsecase, mediaUsecase, runtime)
|
||||||
taskScheduler := worker.NewTaskScheduler(taskUsecase, authorityUsecase, taskExecutor, logger)
|
taskScheduler := worker.NewTaskScheduler(taskUsecase, authorityUsecase, taskExecutor, logger)
|
||||||
taskRuntime := worker.NewTaskRuntime(taskScheduler)
|
taskRuntime := worker.NewTaskRuntime(taskScheduler)
|
||||||
|
|
@ -87,46 +91,46 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
|
||||||
task := handler.NewTask(taskService)
|
task := handler.NewTask(taskService)
|
||||||
mediaService := service.NewMediaService(mediaUsecase, runtimeSettings)
|
mediaService := service.NewMediaService(mediaUsecase, runtimeSettings)
|
||||||
media := handler.NewMedia(mediaService)
|
media := handler.NewMedia(mediaService)
|
||||||
auditQueryRepo := data.NewAuditRepo(dataData)
|
auditQueryRepo := system.NewAuditRepo(dataData)
|
||||||
auditUsecase := biz.NewAuditUsecase(auditQueryRepo)
|
auditUsecase := biz.NewAuditUsecase(auditQueryRepo)
|
||||||
auditService := service.NewAuditService(auditUsecase)
|
auditService := service.NewAuditService(auditUsecase)
|
||||||
auditRecordRepo := data.NewAuditRecorderRepo(dataData)
|
auditRecordRepo := system.NewAuditRecorderRepo(dataData)
|
||||||
auditRecorderUsecase := biz.NewAuditRecorderUsecase(auditRecordRepo)
|
auditRecorderUsecase := biz.NewAuditRecorderUsecase(auditRecordRepo)
|
||||||
auditRecorder := service.NewAuditRecorder(auditRecorderUsecase)
|
auditRecorder := service.NewAuditRecorder(auditRecorderUsecase)
|
||||||
logFileRepo := data.NewLogFileRepo(dataData)
|
logFileRepo := system.NewLogFileRepo(dataData)
|
||||||
logViewerUsecase := biz.NewLogViewerUsecase(logFileRepo)
|
logViewerUsecase := biz.NewLogViewerUsecase(logFileRepo)
|
||||||
logViewerService := service.NewLogViewerService(logViewerUsecase)
|
logViewerService := service.NewLogViewerService(logViewerUsecase)
|
||||||
audit := handler.NewAudit(auditService, auditRecorder, logViewerService, logger)
|
audit := handler.NewAudit(auditService, auditRecorder, logViewerService, logger)
|
||||||
exportRepo := data.NewExportRepo(dataData)
|
exportRepo := system.NewExportRepo(dataData)
|
||||||
exportUsecase := biz.NewExportUsecase(exportRepo)
|
exportUsecase := biz.NewExportUsecase(exportRepo)
|
||||||
cache := data.NewCache(dataData)
|
bizCache := cache.New(dataData)
|
||||||
exportService := service.NewExportService(exportUsecase, cache)
|
exportService := service.NewExportService(exportUsecase, bizCache)
|
||||||
export := handler.NewExport(exportService)
|
export := handler.NewExport(exportService)
|
||||||
versionRepo := data.NewVersionRepo(dataData)
|
versionRepo := system.NewVersionRepo(dataData)
|
||||||
versionUsecase := biz.NewVersionUsecase(versionRepo)
|
versionUsecase := biz.NewVersionUsecase(versionRepo)
|
||||||
versionService := service.NewVersionService(versionUsecase)
|
versionService := service.NewVersionService(versionUsecase)
|
||||||
version := handler.NewVersion(versionService)
|
version := handler.NewVersion(versionService)
|
||||||
dictionaryRepo := data.NewDictionaryRepo(dataData)
|
dictionaryRepo := system.NewDictionaryRepo(dataData)
|
||||||
dictionaryUsecase := biz.NewDictionaryUsecase(dictionaryRepo)
|
dictionaryUsecase := biz.NewDictionaryUsecase(dictionaryRepo)
|
||||||
dictionaryService := service.NewDictionaryService(dictionaryUsecase)
|
dictionaryService := service.NewDictionaryService(dictionaryUsecase)
|
||||||
dictionary := handler.NewDictionary(dictionaryService)
|
dictionary := handler.NewDictionary(dictionaryService)
|
||||||
parameterRepo := data.NewParameterRepo(dataData)
|
parameterRepo := system.NewParameterRepo(dataData)
|
||||||
parameterUsecase := biz.NewParameterUsecase(parameterRepo)
|
parameterUsecase := biz.NewParameterUsecase(parameterRepo)
|
||||||
parameterService := service.NewParameterService(parameterUsecase)
|
parameterService := service.NewParameterService(parameterUsecase)
|
||||||
parameter := handler.NewParameter(parameterService)
|
parameter := handler.NewParameter(parameterService)
|
||||||
apiTokenRepo := data.NewAPITokenRepo(dataData)
|
apiTokenRepo := system.NewAPITokenRepo(dataData)
|
||||||
tokenUsecase := biz.NewTokenUsecase(apiTokenRepo)
|
tokenUsecase := biz.NewTokenUsecase(apiTokenRepo)
|
||||||
tokenIssuer := data.NewTokenIssuer(runtimeSettings)
|
tokenIssuer := system.NewTokenIssuer(runtimeSettings)
|
||||||
tokenService := service.NewTokenService(tokenUsecase, tokenIssuer)
|
tokenService := service.NewTokenService(tokenUsecase, tokenIssuer)
|
||||||
apiToken := handler.NewAPIToken(tokenService)
|
apiToken := handler.NewAPIToken(tokenService)
|
||||||
initializationRepo := data.NewInitializationRepo(dataData)
|
initializationRepo := data.NewInitializationRepo(dataData)
|
||||||
systemConfigUsecase := biz.NewSystemConfigUsecase(initializationRepo, taskRuntime)
|
systemConfigUsecase := biz.NewSystemConfigUsecase(initializationRepo, taskRuntime)
|
||||||
systemConfigService := service.NewSystemConfigService(systemConfigUsecase, runtimeSettings)
|
systemConfigService := service.NewSystemConfigService(systemConfigUsecase, runtimeSettings)
|
||||||
securityRepo := data.NewSecurityRepo(dataData)
|
securityRepo := system.NewSecurityRepo(dataData)
|
||||||
securityUsecase := biz.NewSecurityUsecase(securityRepo, cache, runtimeSettings, tokenUsecase)
|
securityUsecase := biz.NewSecurityUsecase(securityRepo, bizCache, runtimeSettings, tokenUsecase)
|
||||||
securityService := service.NewSecurityService(securityUsecase)
|
securityService := service.NewSecurityService(securityUsecase)
|
||||||
systemConfig := handler.NewSystemConfig(systemConfigService, securityService)
|
systemConfig := handler.NewSystemConfig(systemConfigService, securityService)
|
||||||
userRepo := data.NewUserRepo(dataData)
|
userRepo := system.NewUserRepo(dataData)
|
||||||
userUsecase := biz.NewUserUsecase(userRepo)
|
userUsecase := biz.NewUserUsecase(userRepo)
|
||||||
authenticationUsecase := biz.NewAuthenticationUsecase(userUsecase, securityUsecase, tokenIssuer, auditRecordRepo)
|
authenticationUsecase := biz.NewAuthenticationUsecase(userUsecase, securityUsecase, tokenIssuer, auditRecordRepo)
|
||||||
authService := service.NewAuthService(authenticationUsecase)
|
authService := service.NewAuthService(authenticationUsecase)
|
||||||
|
|
@ -135,7 +139,7 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
|
||||||
user := handler.NewUser(userService, authService)
|
user := handler.NewUser(userService, authService)
|
||||||
navigation := handler.NewNavigation(userService)
|
navigation := handler.NewNavigation(userService)
|
||||||
session := handler.NewSession(tokenService)
|
session := handler.NewSession(tokenService)
|
||||||
set := handler.NewSet(authority, menu, api, permission, organization, announcement, email, payment, task, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session)
|
set := handler.NewSet(authority, menu, api, permission, organization, announcement, handlerEmail, handlerPayment, task, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session)
|
||||||
engine := server.NewGinEngine(runtime, accessControlService, set, authService, securityService, auditRecorder, logger, string2)
|
engine := server.NewGinEngine(runtime, accessControlService, set, authService, securityService, auditRecorder, logger, string2)
|
||||||
httpServer := server.NewGinServer(confServer, engine)
|
httpServer := server.NewGinServer(confServer, engine)
|
||||||
app := newApp(logger, httpServer, taskScheduler, auditRecorder, reloadableLogger)
|
app := newApp(logger, httpServer, taskScheduler, auditRecorder, reloadableLogger)
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -14,8 +14,10 @@ require (
|
||||||
github.com/fsnotify/fsnotify v1.10.1
|
github.com/fsnotify/fsnotify v1.10.1
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
|
github.com/go-gormigrate/gormigrate/v2 v2.1.6
|
||||||
github.com/go-kratos/kratos/contrib/otel/v3 v3.0.0-20260617100506-4e232a3eff59
|
github.com/go-kratos/kratos/contrib/otel/v3 v3.0.0-20260617100506-4e232a3eff59
|
||||||
github.com/go-kratos/kratos/v3 v3.0.0
|
github.com/go-kratos/kratos/v3 v3.0.0
|
||||||
|
github.com/go-pay/crypto v0.0.1
|
||||||
github.com/go-pay/gopay v1.5.122
|
github.com/go-pay/gopay v1.5.122
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
|
@ -95,7 +97,6 @@ require (
|
||||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||||
github.com/go-openapi/spec v0.20.4 // indirect
|
github.com/go-openapi/spec v0.20.4 // indirect
|
||||||
github.com/go-openapi/swag v0.19.15 // indirect
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
github.com/go-pay/crypto v0.0.1 // indirect
|
|
||||||
github.com/go-pay/errgroup v0.0.3 // indirect
|
github.com/go-pay/errgroup v0.0.3 // indirect
|
||||||
github.com/go-pay/smap v0.0.2 // indirect
|
github.com/go-pay/smap v0.0.2 // indirect
|
||||||
github.com/go-pay/util v0.0.4 // indirect
|
github.com/go-pay/util v0.0.4 // indirect
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -126,6 +126,8 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g
|
||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
|
github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg=
|
||||||
|
github.com/go-gormigrate/gormigrate/v2 v2.1.6/go.mod h1:PZpedQc4tWaxn6kvXicwhinh3L0seLpMc5ReKRX5id4=
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||||
github.com/go-kratos/kratos/contrib/otel/v3 v3.0.0-20260617100506-4e232a3eff59 h1:FeDujUZF6a2pPS0RWGI52IXWPqVcGW1QP5vHDlAqLE8=
|
github.com/go-kratos/kratos/contrib/otel/v3 v3.0.0-20260617100506-4e232a3eff59 h1:FeDujUZF6a2pPS0RWGI52IXWPqVcGW1QP5vHDlAqLE8=
|
||||||
|
|
@ -209,6 +211,8 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
|
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||||
|
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
|
|
||||||
|
|
@ -387,6 +387,19 @@ func (uc *PaymentUsecase) Order(ctx context.Context, provider, tradeNo string) (
|
||||||
return uc.orders.FindPaymentOrder(ctx, provider, tradeNo)
|
return uc.orders.FindPaymentOrder(ctx, provider, tradeNo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (uc *PaymentUsecase) Orders(ctx context.Context, page, pageSize int, filter PaymentOrderFilter) ([]*PaymentOrder, int64, error) {
|
||||||
|
if uc.orders == nil {
|
||||||
|
return nil, 0, errors.New("支付订单仓储未接入")
|
||||||
|
}
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
return uc.orders.ListPaymentOrders(ctx, page, pageSize, filter)
|
||||||
|
}
|
||||||
|
|
||||||
func (uc *PaymentUsecase) Create(ctx context.Context, req *PaymentRequest) (*PaymentResult, error) {
|
func (uc *PaymentUsecase) Create(ctx context.Context, req *PaymentRequest) (*PaymentResult, error) {
|
||||||
if req == nil || !validPaymentText(req.Provider, 64) || !validPaymentText(req.TradeNo, 128) || !validPaymentText(req.BusinessType, 64) || !validPaymentText(req.BusinessID, 128) {
|
if req == nil || !validPaymentText(req.Provider, 64) || !validPaymentText(req.TradeNo, 128) || !validPaymentText(req.BusinessType, 64) || !validPaymentText(req.BusinessID, 128) {
|
||||||
return nil, errors.New("支付参数不完整")
|
return nil, errors.New("支付参数不完整")
|
||||||
|
|
|
||||||
|
|
@ -111,9 +111,19 @@ type PaymentProviderUpdate struct {
|
||||||
CreatePayload json.RawMessage
|
CreatePayload json.RawMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PaymentOrderFilter struct {
|
||||||
|
Provider string
|
||||||
|
TradeNo string
|
||||||
|
BusinessType string
|
||||||
|
BusinessID string
|
||||||
|
PaymentStatus string
|
||||||
|
RefundStatus string
|
||||||
|
}
|
||||||
|
|
||||||
type PaymentOrderRepo interface {
|
type PaymentOrderRepo interface {
|
||||||
CreatePaymentOrder(context.Context, *PaymentOrder) (order *PaymentOrder, created bool, err error)
|
CreatePaymentOrder(context.Context, *PaymentOrder) (order *PaymentOrder, created bool, err error)
|
||||||
FindPaymentOrder(context.Context, string, string) (*PaymentOrder, error)
|
FindPaymentOrder(context.Context, string, string) (*PaymentOrder, error)
|
||||||
|
ListPaymentOrders(context.Context, int, int, PaymentOrderFilter) ([]*PaymentOrder, int64, error)
|
||||||
RecordPaymentCreate(context.Context, string, string, *PaymentProviderUpdate) (*PaymentOrder, error)
|
RecordPaymentCreate(context.Context, string, string, *PaymentProviderUpdate) (*PaymentOrder, error)
|
||||||
ApplyPaymentResult(context.Context, string, string, *PaymentProviderUpdate) (*PaymentOrder, error)
|
ApplyPaymentResult(context.Context, string, string, *PaymentProviderUpdate) (*PaymentOrder, error)
|
||||||
BeginPaymentFulfillment(context.Context, string, string, time.Duration) (order *PaymentOrder, token string, duplicate bool, err error)
|
BeginPaymentFulfillment(context.Context, string, string, time.Duration) (order *PaymentOrder, token string, duplicate bool, err error)
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,12 @@ func (r *paymentOrderRepoStub) FindPaymentOrder(context.Context, string, string)
|
||||||
}
|
}
|
||||||
return r.order, nil
|
return r.order, nil
|
||||||
}
|
}
|
||||||
|
func (r *paymentOrderRepoStub) ListPaymentOrders(context.Context, int, int, PaymentOrderFilter) ([]*PaymentOrder, int64, error) {
|
||||||
|
if r.order == nil {
|
||||||
|
return []*PaymentOrder{}, 0, nil
|
||||||
|
}
|
||||||
|
return []*PaymentOrder{r.order}, 1, nil
|
||||||
|
}
|
||||||
func (r *paymentOrderRepoStub) RecordPaymentCreate(_ context.Context, _, _ string, update *PaymentProviderUpdate) (*PaymentOrder, error) {
|
func (r *paymentOrderRepoStub) RecordPaymentCreate(_ context.Context, _, _ string, update *PaymentProviderUpdate) (*PaymentOrder, error) {
|
||||||
r.order.PaymentStatus = PaymentStatusPending
|
r.order.PaymentStatus = PaymentStatusPending
|
||||||
r.order.CreatePayload = append(r.order.CreatePayload[:0], update.CreatePayload...)
|
r.order.CreatePayload = append(r.order.CreatePayload[:0], update.CreatePayload...)
|
||||||
|
|
|
||||||
|
|
@ -1 +1,28 @@
|
||||||
# Data
|
# Data Layer
|
||||||
|
|
||||||
|
`internal/data` is the persistence composition root. It owns the shared
|
||||||
|
database/runtime container (`Data`), database clients, configuration storage,
|
||||||
|
and the Wire `ProviderSet`. Business repositories live in submodules so a new
|
||||||
|
feature can be found by its domain instead of by scanning one large package.
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
- `internal/data/system` contains the built-in administration domain: users,
|
||||||
|
authorities, APIs, permissions, menus, organization, dictionaries,
|
||||||
|
parameters, tokens, security, audit/logging, media, announcements, tasks,
|
||||||
|
versions, exports, bootstrap seeding, and system migrations.
|
||||||
|
- `internal/data/payment` contains payment configuration and payment-order
|
||||||
|
persistence. Provider SDK implementations are separate in
|
||||||
|
`internal/integration/payment`.
|
||||||
|
- `internal/data/migration` contains the version-table runner used by the
|
||||||
|
root migration coordinator.
|
||||||
|
|
||||||
|
The root package intentionally keeps only cross-cutting infrastructure:
|
||||||
|
database lifecycle/reloads, runtime configuration persistence, data-scope
|
||||||
|
auditing, integration configuration storage, and migration orchestration.
|
||||||
|
Repositories depend on narrow module seams (`system.Provider` and
|
||||||
|
`payment.Provider`) rather than importing the root implementation details.
|
||||||
|
|
||||||
|
Non-database adapters remain under `internal/integration` (storage, email,
|
||||||
|
payment SDKs, and cache), while reusable GORM and pagination helpers live in
|
||||||
|
`pkg/gormkit` and `pkg/pagination`.
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,30 @@ import (
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"kra/internal/biz"
|
||||||
"kra/internal/conf"
|
"kra/internal/conf"
|
||||||
|
datapayment "kra/internal/data/payment"
|
||||||
|
datasystem "kra/internal/data/system"
|
||||||
|
"kra/internal/integration/cache"
|
||||||
|
"kra/internal/integration/email"
|
||||||
|
"kra/internal/integration/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ProviderSet = wire.NewSet(NewData, NewRuntimeSettings, NewTokenIssuer, NewUserRepo, NewInitializationRepo, NewAuthorityAccessRepo, NewAPIRepo, NewPermissionRepo, NewMenuRepo, NewDepartmentRepo, NewPositionRepo, NewDictionaryRepo, NewParameterRepo, NewAPITokenRepo, NewSecurityRepo, NewVersionRepo, NewExportRepo, NewAuditRepo, NewAuditRecorderRepo, NewLogFileRepo, NewTaskRepo, NewMediaRepo, NewAnnouncementRepo, NewEmailRepo, NewPaymentRepo, NewPaymentOrderRepo, NewCache, NewFileStorage)
|
var ProviderSet = wire.NewSet(
|
||||||
|
NewData,
|
||||||
|
wire.Bind(new(datasystem.Provider), new(*Data)),
|
||||||
|
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
||||||
|
wire.Bind(new(datapayment.Provider), new(*Data)),
|
||||||
|
wire.Bind(new(cache.RedisProvider), new(*Data)),
|
||||||
|
wire.Bind(new(biz.FileStorage), new(*storage.Reloadable)),
|
||||||
|
datasystem.NewRuntimeSettings,
|
||||||
|
datasystem.NewTokenIssuer,
|
||||||
|
datasystem.NewUserRepo, NewInitializationRepo, datasystem.NewAuthorityAccessRepo, datasystem.NewAPIRepo, datasystem.NewPermissionRepo,
|
||||||
|
datasystem.NewMenuRepo, datasystem.NewDepartmentRepo, datasystem.NewPositionRepo, datasystem.NewDictionaryRepo, datasystem.NewParameterRepo, datasystem.NewAPITokenRepo,
|
||||||
|
datasystem.NewSecurityRepo,
|
||||||
|
datasystem.NewVersionRepo, datasystem.NewExportRepo, datasystem.NewAuditRepo, datasystem.NewAuditRecorderRepo, datasystem.NewLogFileRepo, datasystem.NewTaskRepo,
|
||||||
|
datasystem.NewMediaRepo, datasystem.NewAnnouncementRepo, email.NewEmailRepo, datapayment.NewPaymentRepo, datapayment.NewPaymentOrderRepo, cache.New, storage.NewFileStorage,
|
||||||
|
)
|
||||||
|
|
||||||
type Data struct {
|
type Data struct {
|
||||||
initMu sync.Mutex
|
initMu sync.Mutex
|
||||||
|
|
@ -24,13 +44,50 @@ type Data struct {
|
||||||
redis *reloadableRedis
|
redis *reloadableRedis
|
||||||
mongo *reloadableMongo
|
mongo *reloadableMongo
|
||||||
runtime *conf.Runtime
|
runtime *conf.Runtime
|
||||||
storage *reloadableStorage
|
storage *storage.Reloadable
|
||||||
dbListMu sync.RWMutex
|
dbListMu sync.RWMutex
|
||||||
dbList map[string]*gorm.DB
|
dbList map[string]*gorm.DB
|
||||||
appLogger *slog.Logger
|
appLogger *slog.Logger
|
||||||
auditLog *dataScopeAuditWriter
|
auditLog *dataScopeAuditWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DB exposes the active primary database to narrowly scoped data submodules.
|
||||||
|
func (d *Data) DB() *gorm.DB {
|
||||||
|
if d == nil || d.gormDB == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.gormDB.DB()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DatabaseReady reports whether the configured primary database has been
|
||||||
|
// initialized. It is intentionally small so system repositories do not depend
|
||||||
|
// on the full Data implementation.
|
||||||
|
func (d *Data) DatabaseReady() bool {
|
||||||
|
return d != nil && d.databaseReady.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runtime exposes the immutable runtime configuration snapshot to data
|
||||||
|
// submodules that need system settings while keeping Data itself private.
|
||||||
|
func (d *Data) Runtime() *conf.Runtime {
|
||||||
|
if d == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.runtime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database resolves the primary or a named database for repositories such as
|
||||||
|
// the system export module.
|
||||||
|
func (d *Data) Database(name string) (*gorm.DB, error) {
|
||||||
|
return d.database(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Data) RedisClient() redis.UniversalClient {
|
||||||
|
if d == nil || d.redis == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return d.redis.load()
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Data) logger() *slog.Logger {
|
func (d *Data) logger() *slog.Logger {
|
||||||
if d != nil && d.appLogger != nil {
|
if d != nil && d.appLogger != nil {
|
||||||
return d.appLogger
|
return d.appLogger
|
||||||
|
|
@ -87,7 +144,7 @@ func (d *Data) database(name string) (*gorm.DB, error) {
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewData(runtime *conf.Runtime, appLogger *slog.Logger) (*Data, func(), error) {
|
func NewData(runtime *conf.Runtime, appLogger *slog.Logger, storageManager *storage.Reloadable) (*Data, func(), error) {
|
||||||
if appLogger == nil {
|
if appLogger == nil {
|
||||||
appLogger = slog.Default()
|
appLogger = slog.Default()
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +158,7 @@ func NewData(runtime *conf.Runtime, appLogger *slog.Logger) (*Data, func(), erro
|
||||||
// and /init/initdb remain available.
|
// and /init/initdb remain available.
|
||||||
c.Database = &conf.Data_Database{}
|
c.Database = &conf.Data_Database{}
|
||||||
}
|
}
|
||||||
d := &Data{runtime: runtime, appLogger: appLogger}
|
d := &Data{runtime: runtime, appLogger: appLogger, storage: storageManager}
|
||||||
usingFallback := !databaseConnectionConfigured(c.Database)
|
usingFallback := !databaseConnectionConfigured(c.Database)
|
||||||
var db *gorm.DB
|
var db *gorm.DB
|
||||||
var err error
|
var err error
|
||||||
|
|
@ -154,12 +211,22 @@ func NewData(runtime *conf.Runtime, appLogger *slog.Logger) (*Data, func(), erro
|
||||||
admin.Storage = storageConfig
|
admin.Storage = storageConfig
|
||||||
admin.Email = emailConfig
|
admin.Email = emailConfig
|
||||||
runtime.Replace(c, admin)
|
runtime.Replace(c, admin)
|
||||||
|
activeStorage, storageErr := storage.New(admin)
|
||||||
|
if storageErr != nil {
|
||||||
|
return nil, nil, fmt.Errorf("initialize storage: %w", storageErr)
|
||||||
|
}
|
||||||
|
if storageManager != nil {
|
||||||
|
storageManager.Replace(activeStorage)
|
||||||
|
}
|
||||||
if db.Migrator().HasTable(&integrationConfigPO{}) {
|
if db.Migrator().HasTable(&integrationConfigPO{}) {
|
||||||
if removeErr := d.removeIntegrationConfigFromFile(); removeErr != nil {
|
if removeErr := d.removeIntegrationConfigFromFile(); removeErr != nil {
|
||||||
appLogger.Warn("remove legacy integration configuration from file", "mod", "integration", "error", removeErr)
|
appLogger.Warn("remove legacy integration configuration from file", "mod", "integration", "error", removeErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if storageManager == nil {
|
||||||
|
return nil, nil, fmt.Errorf("storage manager is nil")
|
||||||
|
}
|
||||||
useRedis := admin != nil && admin.System != nil && admin.System.UseRedis
|
useRedis := admin != nil && admin.System != nil && admin.System.UseRedis
|
||||||
d.redis = newReloadableRedis(openRedis(c.Redis, useRedis, appLogger))
|
d.redis = newReloadableRedis(openRedis(c.Redis, useRedis, appLogger))
|
||||||
useMongo := admin != nil && admin.System != nil && admin.System.UseMongo
|
useMongo := admin != nil && admin.System != nil && admin.System.UseMongo
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
"gorm.io/gorm/schema"
|
"gorm.io/gorm/schema"
|
||||||
"kra/internal/conf"
|
"kra/internal/conf"
|
||||||
|
"kra/pkg/gormkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
var databaseNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_$-]*$`)
|
var databaseNamePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_$-]*$`)
|
||||||
|
|
@ -131,7 +132,7 @@ func gormConfig(config *conf.Data_Database, appLogger ...*slog.Logger) *gorm.Con
|
||||||
if len(appLogger) > 0 {
|
if len(appLogger) > 0 {
|
||||||
log = appLogger[0]
|
log = appLogger[0]
|
||||||
}
|
}
|
||||||
return &gorm.Config{Logger: newGormLogger(log, level), NamingStrategy: schema.NamingStrategy{TablePrefix: config.Prefix, SingularTable: config.Singular}}
|
return &gorm.Config{Logger: gormkit.NewLogger(log, level), NamingStrategy: schema.NamingStrategy{TablePrefix: config.Prefix, SingularTable: config.Singular}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func openWithDriver(driver, dsn string, appLogger ...*slog.Logger) (*gorm.DB, error) {
|
func openWithDriver(driver, dsn string, appLogger ...*slog.Logger) (*gorm.DB, error) {
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
package data
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log/slog"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// gormLogger forwards GORM diagnostics through the application logger so SQL
|
|
||||||
// entries participate in the same daily/category files and error sink as the
|
|
||||||
// rest of the service. Slow queries use the administration contract's 200ms
|
|
||||||
// threshold.
|
|
||||||
type gormLogger struct {
|
|
||||||
logger *slog.Logger
|
|
||||||
slowThreshold time.Duration
|
|
||||||
level logger.LogLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
func newGormLogger(log *slog.Logger, level logger.LogLevel) *gormLogger {
|
|
||||||
if log == nil {
|
|
||||||
log = slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
||||||
}
|
|
||||||
return &gormLogger{logger: log, slowThreshold: 200 * time.Millisecond, level: level}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *gormLogger) LogMode(level logger.LogLevel) logger.Interface {
|
|
||||||
next := *g
|
|
||||||
next.level = level
|
|
||||||
return &next
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *gormLogger) Info(ctx context.Context, message string, args ...any) {
|
|
||||||
g.logger.InfoContext(ctx, fmt.Sprintf(message, args...), "mod", "sql", "gorm_logger", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *gormLogger) Warn(ctx context.Context, message string, args ...any) {
|
|
||||||
g.logger.WarnContext(ctx, fmt.Sprintf(message, args...), "mod", "sql", "gorm_logger", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *gormLogger) Error(ctx context.Context, message string, args ...any) {
|
|
||||||
g.logger.ErrorContext(ctx, fmt.Sprintf(message, args...), "mod", "sql", "gorm_logger", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *gormLogger) Trace(ctx context.Context, begin time.Time, query func() (string, int64), queryErr error) {
|
|
||||||
if g.level <= logger.Silent {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
elapsed := time.Since(begin)
|
|
||||||
sql, rows := query()
|
|
||||||
fields := []any{"mod", "sql", "gorm_logger", true, "sql", sql, "rows", rows, "elapsed_ms", elapsed.Milliseconds()}
|
|
||||||
switch {
|
|
||||||
case queryErr != nil && g.level >= logger.Error && !errors.Is(queryErr, logger.ErrRecordNotFound):
|
|
||||||
fields = append(fields, "error", queryErr)
|
|
||||||
g.logger.ErrorContext(ctx, "SQL 执行错误", fields...)
|
|
||||||
case elapsed > g.slowThreshold && g.level >= logger.Warn:
|
|
||||||
g.logger.WarnContext(ctx, "SQL 慢查询", fields...)
|
|
||||||
case g.level >= logger.Info:
|
|
||||||
g.logger.InfoContext(ctx, "SQL", fields...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"kra/pkg/gormkit"
|
||||||
"kra/pkg/logging"
|
"kra/pkg/logging"
|
||||||
|
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
|
|
@ -16,7 +17,7 @@ import (
|
||||||
func TestGORMLoggerUsesApplicationCategories(t *testing.T) {
|
func TestGORMLoggerUsesApplicationCategories(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
appLogger, cleanup := logging.NewZapLogger(root, "application.log", logging.Options{Level: "info", Format: "json"})
|
appLogger, cleanup := logging.NewZapLogger(root, "application.log", logging.Options{Level: "info", Format: "json"})
|
||||||
databaseLogger := newGormLogger(appLogger, logger.Info)
|
databaseLogger := gormkit.NewLogger(appLogger, logger.Info)
|
||||||
databaseLogger.Trace(context.Background(), time.Now(), func() (string, int64) { return "SELECT 1", 1 }, nil)
|
databaseLogger.Trace(context.Background(), time.Now(), func() (string, int64) { return "SELECT 1", 1 }, nil)
|
||||||
databaseLogger.Trace(context.Background(), time.Now(), func() (string, int64) { return "SELECT missing", 0 }, errors.New("database failure"))
|
databaseLogger.Trace(context.Background(), time.Now(), func() (string, int64) { return "SELECT missing", 0 }, errors.New("database failure"))
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
package data
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql/driver"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/schema"
|
|
||||||
)
|
|
||||||
|
|
||||||
// jsonPO selects a native or textual JSON storage type supported by each
|
|
||||||
// configured database while keeping one value shape in the data layer.
|
|
||||||
type jsonPO []byte
|
|
||||||
|
|
||||||
func (jsonPO) GormDataType() string { return "json" }
|
|
||||||
func (jsonPO) GormDBDataType(db *gorm.DB, _ *schema.Field) string {
|
|
||||||
switch db.Dialector.Name() {
|
|
||||||
case "mysql", "sqlite":
|
|
||||||
return "JSON"
|
|
||||||
case "postgres":
|
|
||||||
return "JSONB"
|
|
||||||
case "sqlserver":
|
|
||||||
return "NVARCHAR(MAX)"
|
|
||||||
case "oracle":
|
|
||||||
return "CLOB"
|
|
||||||
default:
|
|
||||||
return "TEXT"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func (j jsonPO) Value() (driver.Value, error) {
|
|
||||||
if len(j) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return string(j), nil
|
|
||||||
}
|
|
||||||
func (j *jsonPO) Scan(value any) error {
|
|
||||||
switch raw := value.(type) {
|
|
||||||
case nil:
|
|
||||||
*j = nil
|
|
||||||
case []byte:
|
|
||||||
*j = append((*j)[:0], raw...)
|
|
||||||
case string:
|
|
||||||
*j = append((*j)[:0], raw...)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("cannot scan JSON from %T", value)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Package migration owns the application database migration runner. Schema
|
||||||
|
// steps remain in internal/data because they need that package's private POs;
|
||||||
|
// this package contains only the reusable versioning mechanism.
|
||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-gormigrate/gormigrate/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TableName = "sys_schema_migrations"
|
||||||
|
|
||||||
|
type Step struct {
|
||||||
|
ID string
|
||||||
|
Migrate func(*gorm.DB) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(db *gorm.DB, steps []Step) error {
|
||||||
|
if db == nil {
|
||||||
|
return fmt.Errorf("database is nil")
|
||||||
|
}
|
||||||
|
migrations := make([]*gormigrate.Migration, 0, len(steps))
|
||||||
|
for _, step := range steps {
|
||||||
|
current := step
|
||||||
|
migrations = append(migrations, &gormigrate.Migration{ID: current.ID, Migrate: current.Migrate})
|
||||||
|
}
|
||||||
|
manager := gormigrate.New(db, &gormigrate.Options{
|
||||||
|
TableName: TableName,
|
||||||
|
IDColumnName: "id",
|
||||||
|
IDColumnSize: 255,
|
||||||
|
UseTransaction: false,
|
||||||
|
ValidateUnknownMigrations: true,
|
||||||
|
}, migrations)
|
||||||
|
if err := manager.Migrate(); err != nil {
|
||||||
|
return fmt.Errorf("apply database migrations: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -1,395 +1,28 @@
|
||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"kra/internal/data/migration"
|
||||||
"strings"
|
datapayment "kra/internal/data/payment"
|
||||||
"time"
|
datasystem "kra/internal/data/system"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// migrateAll is the single data-layer migration entry point. Module-specific
|
||||||
|
// schema work stays with the module that owns its persistent objects.
|
||||||
func migrateAll(db *gorm.DB) error {
|
func migrateAll(db *gorm.DB) error {
|
||||||
if err := migrateLegacyIgnoreAPITable(db); err != nil {
|
return migration.Run(db, []migration.Step{
|
||||||
return err
|
{ID: "202608200001_baseline", Migrate: func(db *gorm.DB) error {
|
||||||
}
|
if err := datasystem.LegacySchemaMigration(db); err != nil {
|
||||||
if err := migrateLegacyAuthorityDepartmentColumns(db); err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := db.AutoMigrate(
|
|
||||||
&userPO{}, &authorityPO{}, &menuPO{}, &userAuthorityPO{}, &authorityMenuPO{}, &menuParameterPO{},
|
|
||||||
&apiPO{}, &ignoredAPIPO{}, &authorityAPIPO{}, &casbinRulePO{}, &menuButtonPO{}, &authorityButtonPO{},
|
|
||||||
&departmentPO{}, &positionPO{}, &userDepartmentPO{}, &userPositionPO{}, &authorityDepartmentPO{},
|
|
||||||
&dictionaryPO{}, &dictionaryDetailPO{}, ¶meterPO{}, &apiTokenPO{}, &jwtBlacklistPO{}, &securityConfigPO{},
|
|
||||||
&integrationConfigPO{},
|
|
||||||
&versionPO{}, &exportTemplatePO{}, &exportConditionPO{}, &exportJoinPO{},
|
|
||||||
&operationPO{}, &loginLogPO{}, &dataAccessLogPO{}, &errorRecordPO{},
|
|
||||||
&taskPO{}, &taskLogPO{}, &mediaPO{}, &categoryPO{}, &uploadSessionPO{}, &uploadChunkPO{},
|
|
||||||
&announcementPO{},
|
|
||||||
&paymentOrderPO{},
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := ensurePaymentIntegrationConfigs(db); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := migrateLegacyAuthorityAPIsToCasbinRules(db); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := normalizeErrorRecordStatuses(db); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := reconcileRootAuthorityAPIs(db); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return reconcileReferenceIndexes(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// migrateLegacyAuthorityDepartmentColumns preserves data created by early
|
|
||||||
// Kra builds, which used shortened join-column names and a composite primary
|
|
||||||
// key. The administration connection model has neither a primary key nor a
|
|
||||||
// uniqueness constraint, so rebuild the small table before AutoMigrate.
|
|
||||||
func migrateLegacyAuthorityDepartmentColumns(db *gorm.DB) error {
|
|
||||||
clean := db.Session(&gorm.Session{NewDB: true})
|
|
||||||
const (
|
|
||||||
table = "sys_authority_departments"
|
|
||||||
backup = "sys_authority_departments_kra_legacy"
|
|
||||||
)
|
|
||||||
// MySQL and Oracle auto-commit DDL. If a prior process stopped between the
|
|
||||||
// rename and cleanup steps, restore the untouched backup first and retry the
|
|
||||||
// migration from a known state.
|
|
||||||
if clean.Migrator().HasTable(backup) {
|
|
||||||
if clean.Migrator().HasTable(table) {
|
|
||||||
if err := clean.Migrator().DropTable(table); err != nil {
|
|
||||||
return fmt.Errorf("remove incomplete authority-department table: %w", err)
|
|
||||||
}
|
}
|
||||||
}
|
return datapayment.Migrate(db)
|
||||||
if err := clean.Migrator().RenameTable(backup, table); err != nil {
|
}},
|
||||||
return fmt.Errorf("restore authority-department backup: %w", err)
|
{ID: "202608200002_data_reconcile", Migrate: func(db *gorm.DB) error {
|
||||||
}
|
if err := datapayment.Reconcile(db); err != nil {
|
||||||
}
|
return err
|
||||||
if !clean.Migrator().HasTable(table) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
authorityColumn := "sys_authority_authority_id"
|
|
||||||
if !tableHasColumn(clean, table, authorityColumn) {
|
|
||||||
if !tableHasColumn(clean, table, "authority_id") {
|
|
||||||
return fmt.Errorf("authority-department table has no authority column")
|
|
||||||
}
|
|
||||||
authorityColumn = "authority_id"
|
|
||||||
}
|
|
||||||
departmentColumn := "sys_department_id"
|
|
||||||
if !tableHasColumn(clean, table, departmentColumn) {
|
|
||||||
if !tableHasColumn(clean, table, "department_id") {
|
|
||||||
return fmt.Errorf("authority-department table has no department column")
|
|
||||||
}
|
|
||||||
departmentColumn = "department_id"
|
|
||||||
}
|
|
||||||
hasPrimaryKey, err := tableHasPrimaryKey(clean, table)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if authorityColumn == "sys_authority_authority_id" && departmentColumn == "sys_department_id" && !hasPrimaryKey {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type relation struct {
|
|
||||||
AuthorityID uint `gorm:"column:authority_id"`
|
|
||||||
DepartmentID uint `gorm:"column:department_id"`
|
|
||||||
}
|
|
||||||
var rows []relation
|
|
||||||
selectColumns := authorityColumn + " AS authority_id, " + departmentColumn + " AS department_id"
|
|
||||||
if err := clean.Table(table).Select(selectColumns).Scan(&rows).Error; err != nil {
|
|
||||||
return fmt.Errorf("read legacy authority-department rows: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rebuild := func(tx *gorm.DB) error {
|
|
||||||
if err := tx.Migrator().RenameTable(table, backup); err != nil {
|
|
||||||
return fmt.Errorf("rename legacy authority-department table: %w", err)
|
|
||||||
}
|
|
||||||
if err := tx.AutoMigrate(&authorityDepartmentPO{}); err != nil {
|
|
||||||
return fmt.Errorf("create authority-department table: %w", err)
|
|
||||||
}
|
|
||||||
if len(rows) > 0 {
|
|
||||||
items := make([]authorityDepartmentPO, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
items = append(items, authorityDepartmentPO{AuthorityID: row.AuthorityID, DepartmentID: row.DepartmentID})
|
|
||||||
}
|
}
|
||||||
if err := tx.Create(&items).Error; err != nil {
|
return datasystem.CurrentDataMigration(db)
|
||||||
return fmt.Errorf("copy authority-department rows: %w", err)
|
}},
|
||||||
}
|
|
||||||
}
|
|
||||||
var count int64
|
|
||||||
if err := tx.Model(&authorityDepartmentPO{}).Count(&count).Error; err != nil {
|
|
||||||
return fmt.Errorf("verify authority-department rows: %w", err)
|
|
||||||
}
|
|
||||||
if count != int64(len(rows)) {
|
|
||||||
return fmt.Errorf("verify authority-department rows: got %d want %d", count, len(rows))
|
|
||||||
}
|
|
||||||
if err := tx.Migrator().DropTable(backup); err != nil {
|
|
||||||
return fmt.Errorf("drop legacy authority-department table: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch clean.Dialector.Name() {
|
|
||||||
case "mysql", "oracle":
|
|
||||||
if err := rebuild(clean); err != nil {
|
|
||||||
restoreErr := restoreAuthorityDepartmentBackup(clean, table, backup)
|
|
||||||
if restoreErr != nil {
|
|
||||||
return fmt.Errorf("%v; restore authority-department backup: %w", err, restoreErr)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return clean.Transaction(rebuild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func restoreAuthorityDepartmentBackup(db *gorm.DB, table, backup string) error {
|
|
||||||
if db.Migrator().HasTable(table) {
|
|
||||||
if err := db.Migrator().DropTable(table); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if db.Migrator().HasTable(backup) {
|
|
||||||
return db.Migrator().RenameTable(backup, table)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableHasPrimaryKey(db *gorm.DB, table string) (bool, error) {
|
|
||||||
columns, err := db.Migrator().ColumnTypes(table)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
for _, column := range columns {
|
|
||||||
if primary, ok := column.PrimaryKey(); ok && primary {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Older builds used a status label outside the administration page's supported
|
|
||||||
// state set, so normalize existing rows during migration.
|
|
||||||
func normalizeErrorRecordStatuses(db *gorm.DB) error {
|
|
||||||
return db.Session(&gorm.Session{NewDB: true}).Model(&errorRecordPO{}).Where("status = ?", "未解决").Update("status", "未处理").Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// migrateLegacyAuthorityAPIsToCasbinRules upgrades the early Kra join-table
|
|
||||||
// representation to the independent Casbin policy table. Keep the legacy
|
|
||||||
// table in place for backwards compatibility, but make casbin_rule the sole
|
|
||||||
// live policy source. Existing policy rows are not duplicated.
|
|
||||||
func migrateLegacyAuthorityAPIsToCasbinRules(db *gorm.DB) error {
|
|
||||||
clean := db.Session(&gorm.Session{NewDB: true})
|
|
||||||
if !clean.Migrator().HasTable(&authorityAPIPO{}) || !clean.Migrator().HasTable(&casbinRulePO{}) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
type legacyPolicy struct {
|
|
||||||
AuthorityID uint
|
|
||||||
Path string
|
|
||||||
Method string
|
|
||||||
}
|
|
||||||
baseQuery := func() *gorm.DB {
|
|
||||||
return clean.Table("sys_authority_apis sa").
|
|
||||||
Select("sa.authority_id, a.path, a.method").
|
|
||||||
Joins("JOIN sys_apis a ON a.id = sa.api_id")
|
|
||||||
}
|
|
||||||
query := baseQuery()
|
|
||||||
// Early Kra schemas stored sys_apis without soft-delete timestamps. The
|
|
||||||
// legacy-policy migration must run before assuming that column exists;
|
|
||||||
// otherwise an upgrade from those schemas cannot start on MySQL.
|
|
||||||
if tableHasColumn(clean, "sys_apis", "deleted_at") {
|
|
||||||
query = query.Where("a.deleted_at IS NULL")
|
|
||||||
}
|
|
||||||
var rows []legacyPolicy
|
|
||||||
if err := query.Find(&rows).Error; err != nil {
|
|
||||||
// A few MySQL-compatible drivers report stale/incomplete metadata from
|
|
||||||
// INFORMATION_SCHEMA during startup. If the optional soft-delete column
|
|
||||||
// was reported present but the join still rejects it, retry using only
|
|
||||||
// columns shared by every legacy schema. This migration must never block
|
|
||||||
// startup of an older database solely because deleted_at is absent.
|
|
||||||
if strings.Contains(strings.ToLower(err.Error()), "unknown column") && strings.Contains(strings.ToLower(err.Error()), "deleted_at") {
|
|
||||||
if retryErr := baseQuery().Find(&rows).Error; retryErr != nil {
|
|
||||||
return retryErr
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, row := range rows {
|
|
||||||
exists, err := policyExists(clean, row.AuthorityID, row.Path, row.Method)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := clean.Create(&casbinRulePO{Ptype: "p", V0: fmt.Sprint(row.AuthorityID), V1: row.Path, V2: row.Method}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// tableHasColumn deliberately inspects the physical table rather than the
|
|
||||||
// model schema. Legacy databases may predate soft-delete columns even though
|
|
||||||
// the current PO includes gorm.DeletedAt. Metadata inspection failures are
|
|
||||||
// treated as "unknown" so callers use the portable query shape.
|
|
||||||
func tableHasColumn(db *gorm.DB, table, column string) bool {
|
|
||||||
columns, err := db.Migrator().ColumnTypes(table)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, item := range columns {
|
|
||||||
if strings.EqualFold(item.Name(), column) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// migrateLegacyIgnoreAPITable upgrades the early Kra-only composite-key
|
|
||||||
// shape (path, method) to the compatible model shape (ID/timestamps/soft
|
|
||||||
// delete). AutoMigrate can add columns but cannot replace an existing
|
|
||||||
// composite primary key portably, so rebuild the small table once while
|
|
||||||
// preserving every existing ignore rule.
|
|
||||||
func migrateLegacyIgnoreAPITable(db *gorm.DB) error {
|
|
||||||
clean := db.Session(&gorm.Session{NewDB: true})
|
|
||||||
if !clean.Migrator().HasTable(&ignoredAPIPO{}) || clean.Migrator().HasColumn(&ignoredAPIPO{}, "id") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
legacyTable := fmt.Sprintf("sys_ignore_apis_legacy_%d", time.Now().UnixNano())
|
|
||||||
type legacyIgnoredAPI struct {
|
|
||||||
Path string
|
|
||||||
Method string
|
|
||||||
}
|
|
||||||
return clean.Transaction(func(tx *gorm.DB) error {
|
|
||||||
if err := tx.Migrator().RenameTable(ignoredAPIPO{}.TableName(), legacyTable); err != nil {
|
|
||||||
return fmt.Errorf("rename legacy ignore API table: %w", err)
|
|
||||||
}
|
|
||||||
if err := tx.AutoMigrate(&ignoredAPIPO{}); err != nil {
|
|
||||||
return fmt.Errorf("create compatible ignore API table: %w", err)
|
|
||||||
}
|
|
||||||
var rows []legacyIgnoredAPI
|
|
||||||
if err := tx.Table(legacyTable).Find(&rows).Error; err != nil {
|
|
||||||
return fmt.Errorf("read legacy ignore API rows: %w", err)
|
|
||||||
}
|
|
||||||
if len(rows) > 0 {
|
|
||||||
items := make([]ignoredAPIPO, 0, len(rows))
|
|
||||||
for _, row := range rows {
|
|
||||||
items = append(items, ignoredAPIPO{Path: row.Path, Method: row.Method})
|
|
||||||
}
|
|
||||||
if err := tx.Create(&items).Error; err != nil {
|
|
||||||
return fmt.Errorf("copy legacy ignore API rows: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := tx.Migrator().DropTable(legacyTable); err != nil {
|
|
||||||
return fmt.Errorf("drop legacy ignore API table: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// reconcileRootAuthorityAPIs is a one-time upgrade path from the former Kra
|
|
||||||
// implementation where authority 888 bypassed policy storage entirely. The compatible behavior
|
|
||||||
// grants its root role through persisted Casbin policies, so when a legacy
|
|
||||||
// database has the root role but no stored API links, materialize the same
|
|
||||||
// policy set and let normal authorization read it thereafter.
|
|
||||||
func reconcileRootAuthorityAPIs(db *gorm.DB) error {
|
|
||||||
clean := db.Session(&gorm.Session{NewDB: true})
|
|
||||||
var authorityCount int64
|
|
||||||
if err := clean.Session(&gorm.Session{NewDB: true}).Model(&authorityPO{}).Where("authority_id = ?", 888).Count(&authorityCount).Error; err != nil || authorityCount == 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var policyCount int64
|
|
||||||
if err := policyScope(clean).Where("v0 = ?", "888").Count(&policyCount).Error; err != nil || policyCount != 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var ignored []ignoredAPIPO
|
|
||||||
if err := clean.Session(&gorm.Session{NewDB: true}).Find(&ignored).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ignoreSet := make(map[string]struct{}, len(ignored))
|
|
||||||
for _, item := range ignored {
|
|
||||||
ignoreSet[item.Method+"\x00"+item.Path] = struct{}{}
|
|
||||||
}
|
|
||||||
var apis []apiPO
|
|
||||||
if err := clean.Session(&gorm.Session{NewDB: true}).Find(&apis).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rules := make([]casbinRulePO, 0, len(apis))
|
|
||||||
for _, api := range apis {
|
|
||||||
if _, ok := ignoreSet[api.Method+"\x00"+api.Path]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rules = append(rules, newPolicyRule(888, api.Path, api.Method))
|
|
||||||
}
|
|
||||||
if len(rules) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return clean.Session(&gorm.Session{NewDB: true}).Create(&rules).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// reconcileReferenceIndexes removes constraints created by older Kra builds
|
|
||||||
// that are not part of the administration data model. Business services own
|
|
||||||
// duplicate checks and their user-facing error messages.
|
|
||||||
func reconcileReferenceIndexes(db *gorm.DB) error {
|
|
||||||
clean := db.Session(&gorm.Session{NewDB: true})
|
|
||||||
obsolete := []struct {
|
|
||||||
model any
|
|
||||||
name string
|
|
||||||
}{
|
|
||||||
{&apiPO{}, "idx_api_path_method"},
|
|
||||||
{&dictionaryPO{}, "idx_sys_dictionaries_type"},
|
|
||||||
{¶meterPO{}, "idx_sys_params_key"},
|
|
||||||
{&apiTokenPO{}, "idx_sys_api_tokens_token"},
|
|
||||||
{&exportTemplatePO{}, "idx_sys_export_templates_template_id"},
|
|
||||||
}
|
|
||||||
for _, item := range obsolete {
|
|
||||||
migrator := clean.Session(&gorm.Session{NewDB: true}).Migrator()
|
|
||||||
if migrator.HasIndex(item.model, item.name) {
|
|
||||||
if err := migrator.DropIndex(item.model, item.name); err != nil {
|
|
||||||
return fmt.Errorf("drop obsolete index %s: %w", item.name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, item := range []struct {
|
|
||||||
name string
|
|
||||||
field string
|
|
||||||
}{{"idx_sys_users_uuid", "UUID"}, {"idx_sys_users_username", "Username"}} {
|
|
||||||
unique, err := indexIsUnique(clean.Session(&gorm.Session{NewDB: true}), &userPO{}, item.name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !unique {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
migrator := clean.Session(&gorm.Session{NewDB: true}).Migrator()
|
|
||||||
if err = migrator.DropIndex(&userPO{}, item.name); err != nil {
|
|
||||||
return fmt.Errorf("drop legacy unique index %s: %w", item.name, err)
|
|
||||||
}
|
|
||||||
if err = migrator.CreateIndex(&userPO{}, item.field); err != nil {
|
|
||||||
return fmt.Errorf("create reference index %s: %w", item.name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func indexIsUnique(db *gorm.DB, model any, name string) (bool, error) {
|
|
||||||
indexes, err := db.Migrator().GetIndexes(model)
|
|
||||||
if err != nil {
|
|
||||||
// Some third-party GORM drivers do not implement index inspection.
|
|
||||||
// Fresh schemas are already correct; skip only the legacy repair there.
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
for _, index := range indexes {
|
|
||||||
if index.Name() == name {
|
|
||||||
unique, known := index.Unique()
|
|
||||||
return known && unique, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
package data
|
|
||||||
|
|
||||||
import "gorm.io/gorm"
|
|
||||||
|
|
||||||
func paginationLimitOffset(page, size, maxSize int) (limit, offset int) {
|
|
||||||
limit = size
|
|
||||||
if maxSize > 0 && size > maxSize {
|
|
||||||
limit = maxSize
|
|
||||||
}
|
|
||||||
if limit <= 0 {
|
|
||||||
return 0, 0
|
|
||||||
}
|
|
||||||
if page <= 0 {
|
|
||||||
page = 1
|
|
||||||
}
|
|
||||||
return limit, (page - 1) * limit
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyPagination(db *gorm.DB, page, size, maxSize int) *gorm.DB {
|
|
||||||
limit, offset := paginationLimitOffset(page, size, maxSize)
|
|
||||||
if limit == 0 {
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
return db.Offset(offset).Limit(limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyRequiredPagination(db *gorm.DB, page, size, maxSize int) *gorm.DB {
|
|
||||||
limit, offset := paginationLimitOffset(page, size, maxSize)
|
|
||||||
return db.Offset(offset).Limit(limit)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package system
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// IgnoredAPI describes an endpoint excluded from the generated root policy.
|
||||||
|
// It deliberately has no GORM tags so the system module can be reused by
|
||||||
|
// bootstrap and migration code without exposing data-layer PO types.
|
||||||
|
type IgnoredAPI struct {
|
||||||
|
Method string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultIgnoredAPIs(staticPath string) []IgnoredAPI {
|
||||||
|
staticRoute := "/" + strings.Trim(staticPath, "/") + "/*filepath"
|
||||||
|
return []IgnoredAPI{
|
||||||
|
{Method: "GET", Path: "/api/freshCasbin"},
|
||||||
|
{Method: "GET", Path: "/health"},
|
||||||
|
{Method: "GET", Path: "/swagger/*any"},
|
||||||
|
{Method: "GET", Path: staticRoute},
|
||||||
|
{Method: "HEAD", Path: staticRoute},
|
||||||
|
{Method: "POST", Path: "/system/reloadSystem"},
|
||||||
|
{Method: "POST", Path: "/base/login"},
|
||||||
|
{Method: "POST", Path: "/base/captcha"},
|
||||||
|
{Method: "POST", Path: "/init/initdb"},
|
||||||
|
{Method: "POST", Path: "/init/checkdb"},
|
||||||
|
{Method: "GET", Path: "/info/getInfoDataSource"},
|
||||||
|
{Method: "GET", Path: "/info/getInfoPublic"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
package data
|
// Package system contains data-layer implementations for system runtime
|
||||||
|
// settings, authentication tokens, and bootstrap definitions.
|
||||||
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
@ -106,21 +108,12 @@ func (i *tokenIssuer) ReissueToken(source *biz.AuthClaims, authorityID uint) (*b
|
||||||
}
|
}
|
||||||
settings := i.settings.JWTSettings()
|
settings := i.settings.JWTSettings()
|
||||||
claims := &adminauth.Claims{
|
claims := &adminauth.Claims{
|
||||||
UUID: source.UUID,
|
UUID: source.UUID, ID: source.ID, Username: source.Username, NickName: source.NickName,
|
||||||
ID: source.ID,
|
AuthorityID: authorityID, BufferTime: int64(source.BufferTime / time.Second), UserType: source.UserType,
|
||||||
Username: source.Username,
|
MustChangePwd: source.MustChangePwd, PasswordVersion: source.PasswordVersion,
|
||||||
NickName: source.NickName,
|
|
||||||
AuthorityID: authorityID,
|
|
||||||
BufferTime: int64(source.BufferTime / time.Second),
|
|
||||||
UserType: source.UserType,
|
|
||||||
MustChangePwd: source.MustChangePwd,
|
|
||||||
PasswordVersion: source.PasswordVersion,
|
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
Audience: jwt.ClaimStrings(append([]string(nil), source.Audience...)),
|
Audience: jwt.ClaimStrings(append([]string(nil), source.Audience...)), Issuer: source.Issuer,
|
||||||
Issuer: source.Issuer,
|
IssuedAt: jwt.NewNumericDate(source.IssuedAt), NotBefore: jwt.NewNumericDate(source.NotBefore), ExpiresAt: jwt.NewNumericDate(source.ExpiresAt),
|
||||||
IssuedAt: jwt.NewNumericDate(source.IssuedAt),
|
|
||||||
NotBefore: jwt.NewNumericDate(source.NotBefore),
|
|
||||||
ExpiresAt: jwt.NewNumericDate(source.ExpiresAt),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
token, err := adminauth.Sign(settings.SigningKey, claims)
|
token, err := adminauth.Sign(settings.SigningKey, claims)
|
||||||
|
|
@ -146,8 +139,7 @@ func (i *tokenIssuer) ParseToken(token string) (*biz.AuthClaims, error) {
|
||||||
return nil, biz.ErrTokenInvalid
|
return nil, biz.ErrTokenInvalid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
audience := make([]string, len(claims.Audience))
|
audience := append([]string(nil), claims.Audience...)
|
||||||
copy(audience, claims.Audience)
|
|
||||||
issuedAt := time.Time{}
|
issuedAt := time.Time{}
|
||||||
if claims.IssuedAt != nil {
|
if claims.IssuedAt != nil {
|
||||||
issuedAt = claims.IssuedAt.Time
|
issuedAt = claims.IssuedAt.Time
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package data
|
package system
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -10,11 +10,14 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type securityRepo struct{ data *Data }
|
// DatabaseProvider is the smaller seam used by security settings, which also
|
||||||
|
// need to work before the primary database is initialized.
|
||||||
|
type DatabaseProvider interface {
|
||||||
|
DB() *gorm.DB
|
||||||
|
DatabaseReady() bool
|
||||||
|
}
|
||||||
|
|
||||||
func NewSecurityRepo(data *Data) biz.SecurityRepo { return &securityRepo{data: data} }
|
type SecurityConfigPO struct {
|
||||||
|
|
||||||
type securityConfigPO struct {
|
|
||||||
ID uint `gorm:"primaryKey"`
|
ID uint `gorm:"primaryKey"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
|
@ -40,37 +43,46 @@ type securityConfigPO struct {
|
||||||
ForceNewUserChangePassword bool `gorm:"default:false"`
|
ForceNewUserChangePassword bool `gorm:"default:false"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (securityConfigPO) TableName() string { return "sys_security_config" }
|
func (SecurityConfigPO) TableName() string { return "sys_security_config" }
|
||||||
|
|
||||||
func defaultSecurityConfig() securityConfigPO {
|
func DefaultSecurityConfig() SecurityConfigPO {
|
||||||
return securityConfigPO{ID: 1, CaptchaTimeout: 3600, KeyLong: 6, ImgWidth: 240, ImgHeight: 80, PwdMinLength: 8, LimitWindow: 60, LimitCount: 30, LockThreshold: 5, LockDuration: 30, PwdExpireDays: 90}
|
return SecurityConfigPO{ID: 1, CaptchaTimeout: 3600, KeyLong: 6, ImgWidth: 240, ImgHeight: 80, PwdMinLength: 8, LimitWindow: 60, LimitCount: 30, LockThreshold: 5, LockDuration: 30, PwdExpireDays: 90}
|
||||||
}
|
}
|
||||||
func securityFromPO(v securityConfigPO) *biz.SecurityConfig {
|
|
||||||
|
type securityRepo struct{ data DatabaseProvider }
|
||||||
|
|
||||||
|
func NewSecurityRepo(data DatabaseProvider) biz.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}
|
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 securityToPO(v *biz.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 securityToPO(v *biz.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) (*biz.SecurityConfig, error) {
|
||||||
if !r.data.databaseReady.Load() {
|
if !r.data.DatabaseReady() {
|
||||||
po := defaultSecurityConfig()
|
po := DefaultSecurityConfig()
|
||||||
po.ID = 0
|
po.ID = 0
|
||||||
return securityFromPO(po), errors.New("数据库未初始化")
|
return securityFromPO(po), errors.New("数据库未初始化")
|
||||||
}
|
}
|
||||||
var po securityConfigPO
|
db := r.data.DB().WithContext(ctx)
|
||||||
err := r.data.gormDB.WithContext(ctx).First(&po, 1).Error
|
var po SecurityConfigPO
|
||||||
|
err := db.First(&po, 1).Error
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
po = defaultSecurityConfig()
|
po = DefaultSecurityConfig()
|
||||||
err = r.data.gormDB.WithContext(ctx).Create(&po).Error
|
err = db.Create(&po).Error
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return securityFromPO(po), nil
|
return securityFromPO(po), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error {
|
func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error {
|
||||||
po := securityToPO(v)
|
po := securityToPO(v)
|
||||||
if err := r.data.gormDB.WithContext(ctx).Save(&po).Error; err != nil {
|
if err := r.data.DB().WithContext(ctx).Save(&po).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.ID, v.CreatedAt, v.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
|
v.ID, v.CreatedAt, v.UpdatedAt = po.ID, po.CreatedAt, po.UpdatedAt
|
||||||
|
|
@ -78,7 +90,5 @@ func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityCo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *securityRepo) BackfillPasswordUpdatedAt(ctx context.Context, at time.Time) error {
|
func (r *securityRepo) BackfillPasswordUpdatedAt(ctx context.Context, at time.Time) error {
|
||||||
return r.data.gormDB.WithContext(ctx).Model(&userPO{}).
|
return r.data.DB().WithContext(ctx).Table("sys_users").Where("password_updated_at IS NULL").Update("password_updated_at", at).Error
|
||||||
Where("password_updated_at IS NULL").
|
|
||||||
Update("password_updated_at", at).Error
|
|
||||||
}
|
}
|
||||||
|
|
@ -4,17 +4,15 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"kra/internal/biz"
|
"kra/internal/biz"
|
||||||
"kra/internal/conf"
|
"kra/internal/conf"
|
||||||
|
datasystem "kra/internal/data/system"
|
||||||
|
"kra/internal/integration/storage"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *initializationRepo) PersistConfig(context.Context) error { return r.data.persistConfig() }
|
func (r *initializationRepo) PersistConfig(context.Context) error { return r.data.persistConfig() }
|
||||||
|
|
@ -31,7 +29,7 @@ func (r *initializationRepo) PersistAdminConfig(ctx context.Context, raw []byte)
|
||||||
next.Email = currentAdmin.Email
|
next.Email = currentAdmin.Email
|
||||||
}
|
}
|
||||||
next.ConfigPath = currentAdmin.ConfigPath
|
next.ConfigPath = currentAdmin.ConfigPath
|
||||||
candidateStorage, err := buildFileStorage(next)
|
candidateStorage, err := storage.New(next)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +46,7 @@ func (r *initializationRepo) PersistAdminConfig(ctx context.Context, raw []byte)
|
||||||
// immediately; the file watcher remains the fallback for external edits.
|
// immediately; the file watcher remains the fallback for external edits.
|
||||||
r.data.runtime.Replace(currentData, next)
|
r.data.runtime.Replace(currentData, next)
|
||||||
if r.data.storage != nil {
|
if r.data.storage != nil {
|
||||||
r.data.storage.replace(candidateStorage)
|
r.data.storage.Replace(candidateStorage)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -70,7 +68,7 @@ func (r *initializationRepo) PersistRuntimeConfig(ctx context.Context, dataRaw,
|
||||||
nextAdmin.Email = currentAdmin.Email
|
nextAdmin.Email = currentAdmin.Email
|
||||||
}
|
}
|
||||||
nextAdmin.ConfigPath = currentAdmin.ConfigPath
|
nextAdmin.ConfigPath = currentAdmin.ConfigPath
|
||||||
candidateStorage, err := buildFileStorage(nextAdmin)
|
candidateStorage, err := storage.New(nextAdmin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +83,7 @@ func (r *initializationRepo) PersistRuntimeConfig(ctx context.Context, dataRaw,
|
||||||
}
|
}
|
||||||
r.data.runtime.Replace(nextData, nextAdmin)
|
r.data.runtime.Replace(nextData, nextAdmin)
|
||||||
if r.data.storage != nil {
|
if r.data.storage != nil {
|
||||||
r.data.storage.replace(candidateStorage)
|
r.data.storage.Replace(candidateStorage)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -141,137 +139,7 @@ func (r *initializationRepo) Initialize(ctx context.Context, input *biz.Database
|
||||||
if err := migrateAll(db); err != nil {
|
if err := migrateAll(db); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
if err := datasystem.SeedSystem(ctx, db, input); err != nil {
|
||||||
rootParentID := uint(0)
|
|
||||||
authority := authorityPO{AuthorityID: 888, AuthorityName: "超级管理员", ParentID: &rootParentID, DataScope: 1, DefaultRouter: "dashboard"}
|
|
||||||
if err := tx.FirstOrCreate(&authority, authorityPO{AuthorityID: 888}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// The root role is seeded with parent_id=0. Older Kra databases used
|
|
||||||
// NULL, which makes the role disappear from the same root-only queries.
|
|
||||||
if err := tx.Model(&authorityPO{}).Where("authority_id = ? AND parent_id IS NULL", 888).Update("parent_id", 0).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
menus := defaultMenus()
|
|
||||||
for i := range menus {
|
|
||||||
if err := tx.Where("name = ?", menus[i].Name).FirstOrCreate(&menus[i]).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var persisted []menuPO
|
|
||||||
if err := tx.Order("sort asc, id asc").Find(&persisted).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
nameID := make(map[string]uint, len(persisted))
|
|
||||||
for _, menu := range persisted {
|
|
||||||
nameID[menu.Name] = menu.ID
|
|
||||||
}
|
|
||||||
for i := range menus {
|
|
||||||
if menus[i].ActiveName == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := tx.Model(&menuPO{}).Where("name = ?", menus[i].Name).Updates(map[string]any{"parent_id": nameID[menus[i].ActiveName], "active_name": ""}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := tx.Where("sys_authority_authority_id = ?", 888).Delete(&authorityMenuPO{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
links := make([]authorityMenuPO, 0, len(persisted))
|
|
||||||
for _, menu := range persisted {
|
|
||||||
links = append(links, authorityMenuPO{SysAuthorityAuthorityID: 888, SysBaseMenuID: menu.ID})
|
|
||||||
}
|
|
||||||
if len(links) > 0 {
|
|
||||||
if err := tx.Create(&links).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var count int64
|
|
||||||
if err := tx.Model(&userPO{}).Where("username = ?", "admin").Count(&count).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if count == 0 {
|
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(input.AdminPassword), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
user := userPO{UUID: uuid.NewString(), Username: "admin", Password: string(hash), NickName: "超级管理员", AuthorityID: 888, Enable: 1, PasswordUpdatedAt: &now}
|
|
||||||
if err := tx.Create(&user).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Create(&userAuthorityPO{SysUserID: user.ID, SysAuthorityAuthorityID: 888}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
enabled := true
|
|
||||||
department := departmentPO{Name: "总公司", ParentID: 0, Ancestors: "0", Sort: 0, Status: &enabled}
|
|
||||||
if err := tx.Where("name = ?", department.Name).FirstOrCreate(&department).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, position := range []positionPO{{Name: "总经理", Code: "CEO", Sort: 1, Status: &enabled}, {Name: "普通员工", Code: "STAFF", Sort: 2, Status: &enabled}} {
|
|
||||||
if err := tx.Where("code = ?", position.Code).FirstOrCreate(&position).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
security := defaultSecurityConfig()
|
|
||||||
if err := tx.FirstOrCreate(&security, securityConfigPO{ID: 1}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
exportTemplate := exportTemplatePO{Name: "api", DBTableName: "sys_apis", TemplateID: "api", TemplateInfo: "{\n\"path\":\"路径\",\n\"method\":\"方法(大写)\",\n\"description\":\"方法介绍\",\n\"api_group\":\"方法分组\"\n}"}
|
|
||||||
if err := tx.Where("template_id = ?", exportTemplate.TemplateID).FirstOrCreate(&exportTemplate).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, task := range []taskPO{{Name: "ClearDB", Description: "定时清理数据库过期日志(操作记录/JWT黑名单/定时任务执行日志)", Spec: "@daily", ExecutorType: "method", MethodName: "ClearDB", Enabled: true}, {Name: "CleanStaleUploads", Description: "定时清理过期大文件上传会话", Spec: "@hourly", ExecutorType: "method", MethodName: "CleanStaleUploads", Enabled: true}} {
|
|
||||||
if err := tx.Where("name = ?", task.Name).FirstOrCreate(&task).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, item := range input.APIs {
|
|
||||||
if item == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup}
|
|
||||||
if err := tx.Where("path = ? AND method = ?", po.Path, po.Method).FirstOrCreate(&po).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
staticPath := "uploads/file"
|
|
||||||
if admin := r.data.runtime.Admin(); admin != nil && admin.Local != nil && strings.Trim(admin.Local.PathPrefix, "/") != "" {
|
|
||||||
staticPath = strings.Trim(admin.Local.PathPrefix, "/")
|
|
||||||
}
|
|
||||||
ignoredAPIs := defaultIgnoredAPIs(staticPath)
|
|
||||||
for _, ignored := range ignoredAPIs {
|
|
||||||
if err := tx.FirstOrCreate(&ignored, ignored).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ignoreSet := make(map[string]struct{}, len(ignoredAPIs))
|
|
||||||
for _, ignored := range ignoredAPIs {
|
|
||||||
ignoreSet[ignored.Method+"\x00"+ignored.Path] = struct{}{}
|
|
||||||
}
|
|
||||||
var apiRows []apiPO
|
|
||||||
if err := tx.Find(&apiRows).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, api := range apiRows {
|
|
||||||
if _, ignored := ignoreSet[api.Method+"\x00"+api.Path]; ignored {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
exists, err := policyExists(tx, 888, api.Path, api.Method)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rule := newPolicyRule(888, api.Path, api.Method)
|
|
||||||
if err := tx.Create(&rule).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
currentAdmin := r.data.runtime.Admin()
|
currentAdmin := r.data.runtime.Admin()
|
||||||
|
|
@ -310,41 +178,3 @@ func (r *initializationRepo) Initialize(ctx context.Context, input *biz.Database
|
||||||
activated = true
|
activated = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultIgnoredAPIs(staticPath string) []ignoredAPIPO {
|
|
||||||
staticRoute := "/" + strings.Trim(staticPath, "/") + "/*filepath"
|
|
||||||
return []ignoredAPIPO{
|
|
||||||
{Method: "GET", Path: "/api/freshCasbin"}, {Method: "GET", Path: "/health"},
|
|
||||||
{Method: "GET", Path: "/swagger/*any"},
|
|
||||||
{Method: "GET", Path: staticRoute}, {Method: "HEAD", Path: staticRoute},
|
|
||||||
{Method: "POST", Path: "/system/reloadSystem"}, {Method: "POST", Path: "/base/login"},
|
|
||||||
{Method: "POST", Path: "/base/captcha"}, {Method: "POST", Path: "/init/initdb"},
|
|
||||||
{Method: "POST", Path: "/init/checkdb"}, {Method: "GET", Path: "/info/getInfoDataSource"},
|
|
||||||
{Method: "GET", Path: "/info/getInfoPublic"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultMenus() []menuPO {
|
|
||||||
root := func(path, name, title, icon string, sort int) menuPO {
|
|
||||||
return menuPO{Path: path, Name: name, Component: "view/routerHolder.vue", Title: title, Icon: icon, Sort: sort}
|
|
||||||
}
|
|
||||||
child := func(parent, path, name, component, title, icon string, sort int) menuPO {
|
|
||||||
return menuPO{MenuLevel: 1, Path: path, Name: name, Component: component, Title: title, Icon: icon, Sort: sort, ActiveName: parent}
|
|
||||||
}
|
|
||||||
cachedChild := func(parent, path, name, component, title, icon string, sort int) menuPO {
|
|
||||||
value := child(parent, path, name, component, title, icon, sort)
|
|
||||||
value.KeepAlive = true
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return []menuPO{
|
|
||||||
{Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Title: "仪表盘", Icon: "odometer", Sort: 1},
|
|
||||||
root("permission", "permission", "权限管理", "perm-kra", 2), root("org", "org", "组织管理", "share", 3), root("systemConfig", "systemConfig", "系统设置", "config-kra", 4), root("monitor", "monitor", "运维监控", "monitor-kra", 5), root("media", "media", "媒体管理", "folder-opened", 6), root("extensions", "extensions", "扩展功能", "cherry", 10),
|
|
||||||
{Path: "person", Name: "person", Component: "view/person/person.vue", Title: "个人信息", Icon: "postcard", Hidden: true, Sort: 13},
|
|
||||||
child("permission", "authority", "authority", "view/superAdmin/authority/authority.vue", "角色管理", "role-kra", 1), cachedChild("permission", "menu", "menu", "view/superAdmin/menu/menu.vue", "菜单管理", "tickets", 2), cachedChild("permission", "api", "api", "view/superAdmin/api/api.vue", "api管理", "api-kra", 3), child("permission", "apiToken", "apiToken", "view/systemTools/apiToken/index.vue", "API Token", "key", 4),
|
|
||||||
child("org", "user", "user", "view/superAdmin/user/user.vue", "用户管理", "user", 1), child("org", "department", "department", "view/superAdmin/department/department.vue", "部门管理", "office-building", 2), child("org", "position", "position", "view/superAdmin/position/position.vue", "岗位管理", "postcard", 3),
|
|
||||||
child("systemConfig", "system", "system", "view/systemTools/system/system.vue", "配置文件", "config-file-kra", 1), child("systemConfig", "dictionary", "dictionary", "view/superAdmin/dictionary/sysDictionary.vue", "字典管理", "notebook", 2), child("systemConfig", "sysParams", "sysParams", "view/superAdmin/params/sysParams.vue", "参数管理", "set-up", 3), child("systemConfig", "security", "security", "view/system/security/index.vue", "安全配置", "security-kra", 4),
|
|
||||||
child("monitor", "operation", "operation", "view/superAdmin/operation/sysOperationRecord.vue", "操作历史", "document", 1), child("monitor", "loginLog", "loginLog", "view/systemTools/loginLog/index.vue", "登录日志", "clock", 2), child("monitor", "sysError", "sysError", "view/systemTools/sysError/sysError.vue", "错误日志", "error-kra", 3), child("monitor", "sysVersion", "sysVersion", "view/systemTools/version/version.vue", "版本管理", "version-kra", 4), child("monitor", "state", "state", "view/system/state.vue", "服务器状态", "server", 5), child("monitor", "dataAccessLog", "dataAccessLog", "view/superAdmin/dataAccessLog/dataAccessLog.vue", "数据权限审计", "warning", 6), child("monitor", "timedTask", "timedTask", "view/systemTools/timedTask/index.vue", "定时任务", "timer", 7), child("monitor", "logViewer", "logViewer", "view/systemTools/logViewer/index.vue", "文件日志", "document", 8),
|
|
||||||
child("media", "upload", "upload", "view/media/upload.vue", "媒体库(上传下载)", "upload", 1), child("media", "chunkUpload", "chunkUpload", "view/media/chunkUpload.vue", "大文件上传", "folder-add", 2),
|
|
||||||
child("extensions", "email", "email", "modules/email/view/index.vue", "邮件发送", "message", 4), child("extensions", "anInfo", "anInfo", "modules/announcement/view/info.vue", "公告管理", "bell", 5),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package data
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
@ -44,6 +44,8 @@ var apiMetadata = map[string]apiMetadataValue{
|
||||||
"GET /menu/getMenuRoles": {group: "菜单", description: "获取菜单关联角色列表"},
|
"GET /menu/getMenuRoles": {group: "菜单", description: "获取菜单关联角色列表"},
|
||||||
"GET /position/findPosition": {group: "岗位", description: "根据ID获取岗位"},
|
"GET /position/findPosition": {group: "岗位", description: "根据ID获取岗位"},
|
||||||
"GET /position/getPositionUsers": {group: "岗位", description: "获取岗位成员ID列表"},
|
"GET /position/getPositionUsers": {group: "岗位", description: "获取岗位成员ID列表"},
|
||||||
|
"GET /payment/configs": {group: "支付", description: "获取支付渠道配置"},
|
||||||
|
"GET /payment/orders": {group: "支付", description: "分页查询支付订单"},
|
||||||
"GET /securityConfig/getSecurityConfig": {group: "安全配置", description: "获取安全配置"},
|
"GET /securityConfig/getSecurityConfig": {group: "安全配置", description: "获取安全配置"},
|
||||||
"GET /sysDictionary/exportSysDictionary": {group: "系统字典", description: "导出字典JSON"},
|
"GET /sysDictionary/exportSysDictionary": {group: "系统字典", description: "导出字典JSON"},
|
||||||
"GET /sysDictionary/findSysDictionary": {group: "系统字典", description: "根据ID获取字典(建议选择)"},
|
"GET /sysDictionary/findSysDictionary": {group: "系统字典", description: "根据ID获取字典(建议选择)"},
|
||||||
|
|
@ -125,6 +127,8 @@ var apiMetadata = map[string]apiMetadataValue{
|
||||||
"POST /menu/getMenuList": {group: "菜单", description: "分页获取基础menu列表"},
|
"POST /menu/getMenuList": {group: "菜单", description: "分页获取基础menu列表"},
|
||||||
"POST /menu/setMenuRoles": {group: "菜单", description: "全量覆盖菜单关联角色列表"},
|
"POST /menu/setMenuRoles": {group: "菜单", description: "全量覆盖菜单关联角色列表"},
|
||||||
"POST /menu/updateBaseMenu": {group: "菜单", description: "更新菜单"},
|
"POST /menu/updateBaseMenu": {group: "菜单", description: "更新菜单"},
|
||||||
|
"POST /payment/config": {group: "支付", description: "保存支付渠道配置"},
|
||||||
|
"POST /payment/order": {group: "支付", description: "查询支付订单"},
|
||||||
"POST /position/createPosition": {group: "岗位", description: "创建岗位"},
|
"POST /position/createPosition": {group: "岗位", description: "创建岗位"},
|
||||||
"POST /position/getPositionList": {group: "岗位", description: "获取岗位列表"},
|
"POST /position/getPositionList": {group: "岗位", description: "获取岗位列表"},
|
||||||
"POST /position/setPositionUsers": {group: "岗位", description: "设置岗位成员(反向分配)"},
|
"POST /position/setPositionUsers": {group: "岗位", description: "设置岗位成员(反向分配)"},
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
|
||||||
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
|
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
|
||||||
t.Fatalf("registered route log count = %d, want %d", got, want)
|
t.Fatalf("registered route log count = %d, want %d", got, want)
|
||||||
}
|
}
|
||||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":185`) {
|
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":186`) {
|
||||||
t.Fatalf("startup route summary is missing: %s", text)
|
t.Fatalf("startup route summary is missing: %s", text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -257,6 +257,7 @@ GET /logViewer/dates
|
||||||
GET /logViewer/files
|
GET /logViewer/files
|
||||||
GET /menu/getMenuRoles
|
GET /menu/getMenuRoles
|
||||||
GET /payment/configs
|
GET /payment/configs
|
||||||
|
GET /payment/orders
|
||||||
GET /position/findPosition
|
GET /position/findPosition
|
||||||
GET /position/getPositionUsers
|
GET /position/getPositionUsers
|
||||||
GET /securityConfig/getSecurityConfig
|
GET /securityConfig/getSecurityConfig
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,20 @@ func (h *Payment) Order(c *gin.Context) {
|
||||||
}
|
}
|
||||||
httpx.OKWithData(c, order)
|
httpx.OKWithData(c, order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Payment) Orders(c *gin.Context) {
|
||||||
|
var req dto.PaymentOrderListRequest
|
||||||
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
|
httpx.Fail(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items, total, page, pageSize, err := h.service.Orders(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
httpx.Fail(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpx.Write(c, httpx.CodeSuccess, httpx.PageResult{List: items, Total: total, Page: page, PageSize: pageSize}, "获取成功")
|
||||||
|
}
|
||||||
func (h *Payment) Create(c *gin.Context) {
|
func (h *Payment) Create(c *gin.Context) {
|
||||||
var req dto.PaymentRequest
|
var req dto.PaymentRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ func RegisterPayment(group, public *gin.RouterGroup, h *handler.Payment) {
|
||||||
payment := group.Group("/payment")
|
payment := group.Group("/payment")
|
||||||
payment.GET("/configs", h.Configs)
|
payment.GET("/configs", h.Configs)
|
||||||
payment.POST("/config", h.SaveConfig)
|
payment.POST("/config", h.SaveConfig)
|
||||||
|
payment.GET("/orders", h.Orders)
|
||||||
payment.POST("/order", h.Order)
|
payment.POST("/order", h.Order)
|
||||||
payment.POST("/create", h.Create)
|
payment.POST("/create", h.Create)
|
||||||
payment.POST("/query", h.Query)
|
payment.POST("/query", h.Query)
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,20 @@ type PaymentConfigRequest struct {
|
||||||
// payment configuration endpoint. Values are already masked by the usecase's
|
// payment configuration endpoint. Values are already masked by the usecase's
|
||||||
// repository boundary.
|
// repository boundary.
|
||||||
type PaymentConfigResponse struct {
|
type PaymentConfigResponse struct {
|
||||||
Provider string `json:"Provider"`
|
Provider string `json:"provider"`
|
||||||
Enabled bool `json:"Enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Values json.RawMessage `json:"Values"`
|
Config json.RawMessage `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaymentOrderListRequest struct {
|
||||||
|
Page int `form:"page"`
|
||||||
|
PageSize int `form:"pageSize"`
|
||||||
|
Provider string `form:"provider"`
|
||||||
|
TradeNo string `form:"tradeNo"`
|
||||||
|
BusinessType string `form:"businessType"`
|
||||||
|
BusinessID string `form:"businessId"`
|
||||||
|
PaymentStatus string `form:"paymentStatus"`
|
||||||
|
RefundStatus string `form:"refundStatus"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentRequest struct {
|
type PaymentRequest struct {
|
||||||
|
|
@ -84,6 +95,7 @@ type PaymentResultResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentOrderResponse struct {
|
type PaymentOrderResponse struct {
|
||||||
|
ID uint64 `json:"ID"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
TradeNo string `json:"tradeNo"`
|
TradeNo string `json:"tradeNo"`
|
||||||
ProviderTradeNo string `json:"providerTradeNo"`
|
ProviderTradeNo string `json:"providerTradeNo"`
|
||||||
|
|
@ -105,10 +117,13 @@ type PaymentOrderResponse struct {
|
||||||
PayerCurrency string `json:"payerCurrency,omitempty"`
|
PayerCurrency string `json:"payerCurrency,omitempty"`
|
||||||
AmountBreakdownKnown bool `json:"amountBreakdownKnown"`
|
AmountBreakdownKnown bool `json:"amountBreakdownKnown"`
|
||||||
PaymentStatus string `json:"paymentStatus"`
|
PaymentStatus string `json:"paymentStatus"`
|
||||||
|
ProviderStatus string `json:"providerStatus"`
|
||||||
FulfillmentStatus string `json:"fulfillmentStatus"`
|
FulfillmentStatus string `json:"fulfillmentStatus"`
|
||||||
RefundStatus string `json:"refundStatus"`
|
RefundStatus string `json:"refundStatus"`
|
||||||
RefundedAmount int64 `json:"refundedAmount"`
|
RefundedAmount int64 `json:"refundedAmount"`
|
||||||
|
RefundRequestedAmount int64 `json:"refundRequestedAmount"`
|
||||||
RefundNo string `json:"refundNo,omitempty"`
|
RefundNo string `json:"refundNo,omitempty"`
|
||||||
|
LastError string `json:"lastError,omitempty"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
PaidAt *time.Time `json:"paidAt,omitempty"`
|
PaidAt *time.Time `json:"paidAt,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ func (s *PaymentService) Configs(ctx context.Context) ([]*dto.PaymentConfigRespo
|
||||||
if config == nil {
|
if config == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
result = append(result, &dto.PaymentConfigResponse{Provider: config.Provider, Enabled: config.Enabled, Values: config.Values})
|
result = append(result, &dto.PaymentConfigResponse{Provider: config.Provider, Enabled: config.Enabled, Config: config.Values})
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ func (s *PaymentService) Order(ctx context.Context, provider, tradeNo string) (*
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &dto.PaymentOrderResponse{
|
return &dto.PaymentOrderResponse{
|
||||||
|
ID: order.ID,
|
||||||
Provider: order.Provider, TradeNo: order.TradeNo, ProviderTradeNo: order.ProviderTradeNo,
|
Provider: order.Provider, TradeNo: order.TradeNo, ProviderTradeNo: order.ProviderTradeNo,
|
||||||
BusinessType: order.BusinessType, BusinessID: order.BusinessID, Subject: order.Subject,
|
BusinessType: order.BusinessType, BusinessID: order.BusinessID, Subject: order.Subject,
|
||||||
PaymentMode: order.PaymentMode, OriginalAmount: order.OriginalAmount,
|
PaymentMode: order.PaymentMode, OriginalAmount: order.OriginalAmount,
|
||||||
|
|
@ -45,12 +46,60 @@ func (s *PaymentService) Order(ctx context.Context, provider, tradeNo string) (*
|
||||||
DiscountAmount: order.DiscountAmount, ProviderDiscountAmount: order.ProviderDiscountAmount,
|
DiscountAmount: order.DiscountAmount, ProviderDiscountAmount: order.ProviderDiscountAmount,
|
||||||
MerchantDiscountAmount: order.MerchantDiscountAmount, SettlementAmount: order.SettlementAmount,
|
MerchantDiscountAmount: order.MerchantDiscountAmount, SettlementAmount: order.SettlementAmount,
|
||||||
Currency: order.Currency, PayerCurrency: order.PayerCurrency, AmountBreakdownKnown: order.AmountBreakdownKnown,
|
Currency: order.Currency, PayerCurrency: order.PayerCurrency, AmountBreakdownKnown: order.AmountBreakdownKnown,
|
||||||
PaymentStatus: order.PaymentStatus, FulfillmentStatus: order.FulfillmentStatus,
|
PaymentStatus: order.PaymentStatus, ProviderStatus: order.ProviderStatus, FulfillmentStatus: order.FulfillmentStatus,
|
||||||
RefundStatus: order.RefundStatus, RefundedAmount: order.RefundedAmount, RefundNo: order.RefundNo,
|
RefundStatus: order.RefundStatus, RefundedAmount: order.RefundedAmount, RefundRequestedAmount: order.RefundRequestedAmount, RefundNo: order.RefundNo, LastError: order.LastError,
|
||||||
CreatedAt: order.CreatedAt, UpdatedAt: order.UpdatedAt, PaidAt: order.PaidAt,
|
CreatedAt: order.CreatedAt, UpdatedAt: order.UpdatedAt, PaidAt: order.PaidAt,
|
||||||
FulfilledAt: order.FulfilledAt, RefundedAt: order.RefundedAt,
|
FulfilledAt: order.FulfilledAt, RefundedAt: order.RefundedAt,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *PaymentService) Orders(ctx context.Context, req *dto.PaymentOrderListRequest) ([]*dto.PaymentOrderResponse, int64, int, int, error) {
|
||||||
|
if req == nil {
|
||||||
|
return nil, 0, 1, 10, errors.New("支付订单列表请求为空")
|
||||||
|
}
|
||||||
|
page, pageSize := req.Page, req.PageSize
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if pageSize <= 0 {
|
||||||
|
pageSize = 10
|
||||||
|
}
|
||||||
|
items, total, err := s.uc.Orders(ctx, page, pageSize, biz.PaymentOrderFilter{
|
||||||
|
Provider: req.Provider, TradeNo: req.TradeNo, BusinessType: req.BusinessType,
|
||||||
|
BusinessID: req.BusinessID, PaymentStatus: req.PaymentStatus, RefundStatus: req.RefundStatus,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, page, pageSize, err
|
||||||
|
}
|
||||||
|
result := make([]*dto.PaymentOrderResponse, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
converted, convertErr := paymentOrderResponse(item)
|
||||||
|
if convertErr != nil {
|
||||||
|
return nil, 0, page, pageSize, convertErr
|
||||||
|
}
|
||||||
|
result = append(result, converted)
|
||||||
|
}
|
||||||
|
return result, total, page, pageSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func paymentOrderResponse(order *biz.PaymentOrder) (*dto.PaymentOrderResponse, error) {
|
||||||
|
if order == nil {
|
||||||
|
return nil, errors.New("支付订单为空")
|
||||||
|
}
|
||||||
|
return &dto.PaymentOrderResponse{
|
||||||
|
ID: order.ID, Provider: order.Provider, TradeNo: order.TradeNo, ProviderTradeNo: order.ProviderTradeNo,
|
||||||
|
BusinessType: order.BusinessType, BusinessID: order.BusinessID, Subject: order.Subject,
|
||||||
|
PaymentMode: order.PaymentMode, 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: order.PaymentStatus, ProviderStatus: order.ProviderStatus, FulfillmentStatus: order.FulfillmentStatus,
|
||||||
|
RefundStatus: order.RefundStatus, RefundedAmount: order.RefundedAmount, RefundRequestedAmount: order.RefundRequestedAmount,
|
||||||
|
RefundNo: order.RefundNo, LastError: order.LastError, CreatedAt: order.CreatedAt, UpdatedAt: order.UpdatedAt,
|
||||||
|
PaidAt: order.PaidAt, FulfilledAt: order.FulfilledAt, RefundedAt: order.RefundedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
func (s *PaymentService) Create(ctx context.Context, req *dto.PaymentRequest) (*dto.PaymentResultResponse, error) {
|
func (s *PaymentService) Create(ctx context.Context, req *dto.PaymentRequest) (*dto.PaymentResultResponse, error) {
|
||||||
if req == nil {
|
if req == nil {
|
||||||
return nil, errors.New("支付请求为空")
|
return nil, errors.New("支付请求为空")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Package gormkit contains reusable GORM integration helpers.
|
||||||
|
package gormkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql/driver"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
"gorm.io/gorm/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSON is a portable JSON value for GORM models. It selects a native JSON
|
||||||
|
// column where the driver supports one and a textual fallback elsewhere.
|
||||||
|
type JSON []byte
|
||||||
|
|
||||||
|
func (JSON) GormDataType() string { return "json" }
|
||||||
|
func (JSON) GormDBDataType(db *gorm.DB, _ *schema.Field) string {
|
||||||
|
switch db.Dialector.Name() {
|
||||||
|
case "mysql", "sqlite":
|
||||||
|
return "JSON"
|
||||||
|
case "postgres":
|
||||||
|
return "JSONB"
|
||||||
|
case "sqlserver":
|
||||||
|
return "NVARCHAR(MAX)"
|
||||||
|
case "oracle":
|
||||||
|
return "CLOB"
|
||||||
|
default:
|
||||||
|
return "TEXT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (j JSON) Value() (driver.Value, error) {
|
||||||
|
if len(j) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return string(j), nil
|
||||||
|
}
|
||||||
|
func (j *JSON) Scan(value any) error {
|
||||||
|
switch raw := value.(type) {
|
||||||
|
case nil:
|
||||||
|
*j = nil
|
||||||
|
case []byte:
|
||||||
|
*j = append((*j)[:0], raw...)
|
||||||
|
case string:
|
||||||
|
*j = append((*j)[:0], raw...)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cannot scan JSON from %T", value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger forwards GORM diagnostics through an application slog logger.
|
||||||
|
type Logger struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
slowThreshold time.Duration
|
||||||
|
level logger.LogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger(log *slog.Logger, level logger.LogLevel) *Logger {
|
||||||
|
if log == nil {
|
||||||
|
log = slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||||
|
}
|
||||||
|
return &Logger{logger: log, slowThreshold: 200 * time.Millisecond, level: level}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Logger) LogMode(level logger.LogLevel) logger.Interface {
|
||||||
|
next := *g
|
||||||
|
next.level = level
|
||||||
|
return &next
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Logger) Info(ctx context.Context, message string, args ...any) {
|
||||||
|
g.logger.InfoContext(ctx, fmt.Sprintf(message, args...), "mod", "sql", "gorm_logger", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Logger) Warn(ctx context.Context, message string, args ...any) {
|
||||||
|
g.logger.WarnContext(ctx, fmt.Sprintf(message, args...), "mod", "sql", "gorm_logger", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Logger) Error(ctx context.Context, message string, args ...any) {
|
||||||
|
g.logger.ErrorContext(ctx, fmt.Sprintf(message, args...), "mod", "sql", "gorm_logger", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Logger) Trace(ctx context.Context, begin time.Time, query func() (string, int64), queryErr error) {
|
||||||
|
if g.level <= logger.Silent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elapsed := time.Since(begin)
|
||||||
|
sql, rows := query()
|
||||||
|
fields := []any{"mod", "sql", "gorm_logger", true, "sql", sql, "rows", rows, "elapsed_ms", elapsed.Milliseconds()}
|
||||||
|
switch {
|
||||||
|
case queryErr != nil && g.level >= logger.Error && !errors.Is(queryErr, logger.ErrRecordNotFound):
|
||||||
|
fields = append(fields, "error", queryErr)
|
||||||
|
g.logger.ErrorContext(ctx, "SQL 执行错误", fields...)
|
||||||
|
case elapsed > g.slowThreshold && g.level >= logger.Warn:
|
||||||
|
g.logger.WarnContext(ctx, "SQL 慢查询", fields...)
|
||||||
|
case g.level >= logger.Info:
|
||||||
|
g.logger.InfoContext(ctx, "SQL", fields...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Package pagination contains database-agnostic page calculations shared by
|
||||||
|
// repository implementations.
|
||||||
|
package pagination
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
// LimitOffset normalizes a page request and applies the optional maximum page
|
||||||
|
// size. A non-positive size means that no limit should be applied.
|
||||||
|
func LimitOffset(page, size, maxSize int) (limit, offset int) {
|
||||||
|
limit = size
|
||||||
|
if maxSize > 0 && size > maxSize {
|
||||||
|
limit = maxSize
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
return limit, (page - 1) * limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply applies pagination only when the caller supplied a positive size.
|
||||||
|
func Apply(db *gorm.DB, page, size, maxSize int) *gorm.DB {
|
||||||
|
limit, offset := LimitOffset(page, size, maxSize)
|
||||||
|
if limit == 0 {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
return db.Offset(offset).Limit(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyRequired always applies LIMIT/OFFSET, including LIMIT 0. This keeps
|
||||||
|
// the existing repository contract for APIs whose page size is mandatory.
|
||||||
|
func ApplyRequired(db *gorm.DB, page, size, maxSize int) *gorm.DB {
|
||||||
|
limit, offset := LimitOffset(page, size, maxSize)
|
||||||
|
return db.Offset(offset).Limit(limit)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import service from '@/utils/request'
|
||||||
|
|
||||||
|
export const getPaymentConfigs = () => service({ url: '/payment/configs', method: 'get' })
|
||||||
|
|
||||||
|
export const savePaymentConfig = (data) => service({
|
||||||
|
url: '/payment/config',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getPaymentOrders = (params) => service({
|
||||||
|
url: '/payment/orders',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getPaymentOrder = (data) => service({
|
||||||
|
url: '/payment/order',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="kra-table-box">
|
||||||
|
<el-alert title="支付密钥只用于保存和调用渠道,列表接口会自动脱敏。提交 ****** 会保留原密钥。" type="warning" :closable="false" class="mb-4" />
|
||||||
|
<el-table :data="configs" row-key="provider" stripe>
|
||||||
|
<el-table-column prop="provider" label="渠道" width="160" />
|
||||||
|
<el-table-column label="状态" width="110"><template #default="scope"><el-switch v-model="scope.row.enabled" @change="save(scope.row)" /></template></el-table-column>
|
||||||
|
<el-table-column label="配置 JSON" min-width="520"><template #default="scope"><el-input v-model="scope.row.editor" type="textarea" :rows="4" spellcheck="false" /></template></el-table-column>
|
||||||
|
<el-table-column label="操作" width="110"><template #default="scope"><el-button type="primary" link icon="check" @click="save(scope.row)">保存</el-button></template></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { getPaymentConfigs, savePaymentConfig } from '@/api/payment'
|
||||||
|
|
||||||
|
const configs = ref([])
|
||||||
|
const load = async () => {
|
||||||
|
const res = await getPaymentConfigs()
|
||||||
|
if (res.code === 0) configs.value = (res.data || []).map((item) => ({ ...item, editor: JSON.stringify(item.config || {}, null, 2) }))
|
||||||
|
}
|
||||||
|
const save = async (row) => {
|
||||||
|
let config
|
||||||
|
try { config = JSON.parse(row.editor || '{}') } catch { ElMessage.error('配置必须是合法 JSON'); return }
|
||||||
|
const res = await savePaymentConfig({ provider: row.provider, enabled: row.enabled, config })
|
||||||
|
if (res.code === 0) { ElMessage.success('保存成功'); await load() }
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="kra-search-box">
|
||||||
|
<el-form :inline="true" :model="searchInfo">
|
||||||
|
<el-form-item label="渠道"><el-input v-model="searchInfo.provider" placeholder="alipay / wechat-v3" clearable /></el-form-item>
|
||||||
|
<el-form-item label="商户订单号"><el-input v-model="searchInfo.tradeNo" clearable /></el-form-item>
|
||||||
|
<el-form-item label="业务类型"><el-input v-model="searchInfo.businessType" clearable /></el-form-item>
|
||||||
|
<el-form-item label="支付状态">
|
||||||
|
<el-select v-model="searchInfo.paymentStatus" clearable placeholder="全部">
|
||||||
|
<el-option label="待支付" value="pending" /><el-option label="已支付" value="paid" />
|
||||||
|
<el-option label="失败" value="failed" /><el-option label="已退款" value="refunded" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item><el-button type="primary" icon="search" @click="reload">查询</el-button><el-button icon="refresh" @click="reset">重置</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
<div class="kra-table-box">
|
||||||
|
<el-table :data="rows" row-key="ID" stripe>
|
||||||
|
<el-table-column prop="tradeNo" label="商户订单号" min-width="190" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="provider" label="渠道" width="120" />
|
||||||
|
<el-table-column prop="subject" label="商品/标题" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="金额" width="130"><template #default="scope">{{ scope.row.amount }} {{ scope.row.currency }}</template></el-table-column>
|
||||||
|
<el-table-column label="支付状态" width="110"><template #default="scope"><el-tag :type="statusType(scope.row.paymentStatus)">{{ statusText(scope.row.paymentStatus) }}</el-tag></template></el-table-column>
|
||||||
|
<el-table-column label="发货" width="110"><template #default="scope"><el-tag :type="scope.row.fulfillmentStatus === 'succeeded' ? 'success' : 'info'">{{ scope.row.fulfillmentStatus }}</el-tag></template></el-table-column>
|
||||||
|
<el-table-column label="退款" width="110"><template #default="scope">{{ scope.row.refundStatus }}</template></el-table-column>
|
||||||
|
<el-table-column prop="businessId" label="业务 ID" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="创建时间" width="180"><template #default="scope">{{ formatDate(scope.row.createdAt) }}</template></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="kra-pagination"><el-pagination :current-page="page" :page-size="pageSize" :page-sizes="[10, 30, 50, 100]" :total="total" layout="total, sizes, prev, pager, next, jumper" @current-change="changePage" @size-change="changeSize" /></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { getPaymentOrders } from '@/api/payment'
|
||||||
|
import { formatDate } from '@/utils/format'
|
||||||
|
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const total = ref(0)
|
||||||
|
const rows = ref([])
|
||||||
|
const searchInfo = ref({})
|
||||||
|
const statusText = (status) => ({ initialized: '初始化', pending: '待支付', paid: '已支付', failed: '失败', closed: '已关闭', partially_refunded: '部分退款', refunded: '已退款' }[status] || status || '-')
|
||||||
|
const statusType = (status) => ({ paid: 'success', refunded: 'success', failed: 'danger', closed: 'info' }[status] || 'warning')
|
||||||
|
const load = async () => {
|
||||||
|
const res = await getPaymentOrders({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
|
||||||
|
if (res.code === 0) { rows.value = res.data.list || []; total.value = res.data.total || 0; page.value = res.data.page || page.value; pageSize.value = res.data.pageSize || pageSize.value }
|
||||||
|
}
|
||||||
|
const reload = () => { page.value = 1; load() }
|
||||||
|
const reset = () => { searchInfo.value = {}; reload() }
|
||||||
|
const changePage = (value) => { page.value = value; load() }
|
||||||
|
const changeSize = (value) => { pageSize.value = value; page.value = 1; load() }
|
||||||
|
load()
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue