优化结构

This commit is contained in:
Yvan 2026-08-24 13:56:06 +08:00
parent 0b6b8ddf1a
commit acaee94858
21 changed files with 249 additions and 3025 deletions

View File

@ -9,7 +9,8 @@ agents must follow when changing the template.
api/<domain>/<version>/ Proto sources and generated stubs. Public contract.
cmd/<app>/ Entrypoint, Wire injector, main.go.
configs/ Runtime config (config.yaml). No secrets.
internal/conf/ Config proto; generated by `make config`.
internal/config/ Viper config models, loading, snapshots, and reloads.
internal/global/ Process-wide shared resource registry.
internal/server/ HTTP/gRPC server wiring.
internal/service/ Transport adapters; one file per resource.
internal/biz/ Domain models, usecases, repo interfaces, errors.
@ -129,7 +130,7 @@ tests exercise repo implementations at the storage boundary.
## Generation & generated files
Regenerate via `make api`, `make config`, or `make all`; never hand-edit
Regenerate via `make api` or `make all`; never hand-edit
`*.pb.go`, `*_grpc.pb.go`, `*_http.pb.go`, or `wire_gen.go`.
## Naming & error reasons

View File

@ -9,7 +9,8 @@ agents must follow when changing the template.
api/<domain>/<version>/ Proto sources and generated stubs. Public contract.
cmd/<app>/ Entrypoint, Wire injector, main.go.
configs/ Runtime config (config.yaml). No secrets.
internal/conf/ Config proto; generated by `make config`.
internal/config/ Viper config models, loading, snapshots, and reloads.
internal/global/ Process-wide shared resource registry.
internal/server/ HTTP/gRPC server wiring.
internal/service/ Transport adapters; one file per resource.
internal/biz/ Domain models, usecases, repo interfaces, errors.
@ -120,7 +121,7 @@ tests exercise repo implementations at the storage boundary.
## Generation & generated files
Regenerate via `make api`, `make config`, or `make all`; never hand-edit
Regenerate via `make api` or `make all`; never hand-edit
`*.pb.go`, `*_grpc.pb.go`, `*_http.pb.go`, or `wire_gen.go`.
## Naming & error reasons

View File

@ -8,11 +8,6 @@ init:
go install github.com/google/wire/cmd/wire@latest
go install github.com/bufbuild/buf/cmd/buf@latest
.PHONY: config
# generate internal proto
config:
buf generate --template buf.gen.config.yaml
.PHONY: api
# generate api proto
api:
@ -33,7 +28,6 @@ generate:
# generate all
all:
make api
make config
make generate
# show help

View File

@ -8,7 +8,10 @@ Kra 是基于 Kratos 生命周期与 Wire 依赖注入、使用 Gin 提供管理
cmd/ 服务入口与 Wire
configs/ 运行配置
internal/biz/ 领域对象、用例和仓储接口(按 system/payment/integration/task 拆分)
internal/config/ Viper 配置模型、加载和热更新
internal/data/ 数据库、缓存、对象存储及仓储实现
internal/global/ 进程级共享资源入口
internal/initialize/ 首次安装、配置保存和重载编排
internal/server/ Gin 服务、路由、中间件和 Handler
internal/service/ 按业务模块组织的 HTTP 输入输出与领域对象转换
internal/worker/ 定时任务调度与后台工作进程
@ -39,12 +42,6 @@ pnpm dev
## 生成代码
修改 `internal/conf/conf.proto` 后执行:
```shell
make config
```
修改 Wire Provider 或构造函数后执行:
```shell

View File

@ -1,8 +0,0 @@
version: v2
inputs:
- directory: internal
plugins:
- local: ["go", "run", "google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11"]
out: internal
opt: paths=source_relative

View File

@ -1,6 +1,5 @@
version: v2
modules:
- path: api
- path: internal
deps:
- buf.build/googleapis/googleapis

30
cmd/wire_gen.go generated
View File

@ -13,7 +13,7 @@ import (
payment2 "kra/internal/biz/payment"
system2 "kra/internal/biz/system"
task2 "kra/internal/biz/task"
"kra/internal/conf"
"kra/internal/config"
"kra/internal/data"
"kra/internal/data/integration"
"kra/internal/data/payment"
@ -46,13 +46,13 @@ import (
// Injectors from wire.go:
// wireApp init kratos application.
func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger, reloadableLogger *logging.ReloadableLogger, string2 string) (*kratos.App, func(), error) {
reloadable, err := storage.NewFileStorage(runtime)
func wireApp(configServer *config.Server, store *config.Store, logger *slog.Logger, reloadableLogger *logging.ReloadableLogger, string2 string) (*kratos.App, func(), error) {
reloadable, err := storage.NewFileStorage(store)
if err != nil {
return nil, nil, err
}
catalog := modules.Catalog()
dataData, cleanup, err := data.NewData(runtime, logger, reloadable, catalog)
dataData, cleanup, err := data.NewData(store, logger, reloadable, catalog)
if err != nil {
return nil, nil, err
}
@ -64,7 +64,7 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
userUsecase := system2.NewUserUsecase(userRepo)
securityRepo := system.NewSecurityRepo(dataData)
systemCache := cache.New(dataData)
runtimeSettings := system.NewRuntimeSettings(runtime)
runtimeSettings := system.NewRuntimeSettings(store)
apiTokenRepo := system.NewAPITokenRepo(dataData)
tokenUsecase := system2.NewTokenUsecase(apiTokenRepo)
securityUsecase := system2.NewSecurityUsecase(securityRepo, systemCache, runtimeSettings, tokenUsecase)
@ -100,7 +100,7 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
announcementUsecase := system2.NewAnnouncementUsecase(announcementRepo)
v10 := system3.NewAnnouncementService(announcementUsecase)
announcement := handler.NewAnnouncement(v10)
emailRepo := email.NewEmailRepo(runtime)
emailRepo := email.NewEmailRepo(store)
emailUsecase := system2.NewEmailUsecase(emailRepo)
v11 := system3.NewEmailService(emailUsecase)
handlerEmail := handler.NewEmail(v11)
@ -159,8 +159,8 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
navigation := handler.NewNavigation(v23)
session := handler.NewSession(v21)
integrationConfigRepo := integration.NewIntegrationConfigRepo(dataData)
store := data.NewIntegrationRuntime(dataData)
connectivityTester := integration2.NewConnectivityTester(store)
runtimeconfigStore := data.NewIntegrationRuntime(dataData)
connectivityTester := integration2.NewConnectivityTester(runtimeconfigStore)
integrationConfigUsecase := integration3.NewIntegrationConfigUsecase(integrationConfigRepo, connectivityTester)
v24 := integration4.NewIntegrationConfigService(integrationConfigUsecase)
integrationConfig := handler.NewIntegrationConfig(v24)
@ -168,17 +168,17 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
routes := router.NewRoutes(v25)
maintenanceRepo := system.NewMaintenanceRepo(dataData)
maintenanceUsecase := system2.NewMaintenanceUsecase(maintenanceRepo)
taskMethods := worker.NewTaskMethods(taskUsecase, maintenanceUsecase, mediaUsecase, runtime)
appRuntimeContributions := runtimeContributions(routes, taskMethods)
moduleRuntime := app.Runtime(appRuntimeContributions, registry)
websocketServer, cleanup2, err := websocket.New(store)
taskMethods := worker.NewTaskMethods(taskUsecase, maintenanceUsecase, mediaUsecase, store)
composition := runtimeContributions(routes, taskMethods)
runtime := app.Build(composition, registry)
websocketServer, cleanup2, err := websocket.New(runtimeconfigStore)
if err != nil {
cleanup()
return nil, nil, err
}
engine := server.NewGinEngineWithRuntime(runtime, v, authService, v2, v3, logger, string2, moduleRuntime, websocketServer)
httpServer := server.NewGinServer(confServer, engine)
mqReloadable, cleanup3, err := mq.New(store, logger)
engine := server.NewGinEngineWithRuntime(store, v, authService, v2, v3, logger, string2, runtime, websocketServer)
httpServer := server.NewGinServer(configServer, engine)
mqReloadable, cleanup3, err := mq.New(runtimeconfigStore, logger)
if err != nil {
cleanup2()
cleanup()

View File

@ -1,7 +1,7 @@
# `internal` 目录结构优化结论
参考 Go Kratos 的分层方式,顶层保留 `app`、`modules`、`biz`、`conf`、`data`、
`initialize`、`integration`、`security`、`server`、`service`、`worker` 十个
参考 Go Kratos 的分层方式,顶层保留 `app`、`modules`、`biz`、`config`、`global`、
`data`、`initialize`、`integration`、`security`、`server`、`service`、`worker`
稳定职责。目录不是越少越好:同一技术角色文件较多时,应在所属层下分组,避免
一个目录堆积几十个文件。
@ -9,11 +9,12 @@
```text
internal/
app/ # 运行时组合根
app/ # 应用组合根
modules/ # 静态 catalog 和模块定义
modules/payment/ # payment 模块定义
biz/ # DO、usecase、repo interface
conf/ # 配置 proto/runtime
config/ # Viper 配置、快照和热更新
global/ # 进程级共享资源
data/ # PO、repo、数据库和迁移
initialize/ # 首次安装和配置编排
integration/ # 外部 I/O provider
@ -40,15 +41,15 @@ internal/
独立边界时不继续拆分。
- 删除只转发 `pkg/protoutil``utils/configutil`
## `internal/app` 为什么只保留运行时组合
## `internal/app` 为什么只保留应用组合
`modules/catalog.go` 是静态模块注册点,负责按依赖顺序汇总各模块
`Definition()``app/runtime.go` 只负责任务注册和依赖注入后的运行时路由组合。
`Definition()``app/runtime.go` 只负责任务注册和依赖注入后的路由组合。
这样模块定义不再和应用组合逻辑混在一起,也不能误并入 `biz`、`service` 或
`data`
Catalog 只能自动汇总静态模块贡献;新增模块若提供运行时路由或依赖型任务,仍需
在 cmd/Wire 中显式注册,直到统一的 runtime contribution 协议落地
在 cmd/Wire 中显式注册。
## 其他目录审查

View File

@ -65,7 +65,7 @@ func (s MediaSettings) EffectiveMaxFileSize() int64 {
}
// RuntimeSettings exposes only the active values needed by the application.
// The data implementation resolves every call from conf.Runtime so hot reloads
// The data implementation resolves every call from config.Store so hot reloads
// take effect without rebuilding services.
type RuntimeSettings interface {
RouterPrefix() string

2322
internal/conf/conf.pb.go generated

File diff suppressed because it is too large Load Diff

View File

@ -1,218 +0,0 @@
syntax = "proto3";
package kratos.api;
import "google/protobuf/duration.proto";
option go_package = "kra/internal/conf;conf";
message Bootstrap {
Server server = 1;
Data data = 2;
AdminBackend admin = 3;
}
message Server {
message HTTP {
string network = 1;
string addr = 2;
google.protobuf.Duration timeout = 3;
}
HTTP http = 1;
}
message Data {
message Database {
string driver = 1;
string source = 2;
string host = 3;
string port = 4;
string user = 5;
string password = 6;
string name = 7;
string config = 8;
string path = 9;
string alias_name = 10;
bool disable = 11;
string prefix = 12;
string engine = 13;
string log_mode = 14;
int32 max_idle_conns = 15;
int32 max_open_conns = 16;
int32 conn_max_lifetime = 17;
bool singular = 18;
}
message Redis {
string network = 1;
string addr = 2;
google.protobuf.Duration read_timeout = 3;
google.protobuf.Duration write_timeout = 4;
string name = 5;
string password = 6;
int32 db = 7;
bool use_cluster = 8;
repeated string cluster_addrs = 9;
}
message MongoHost {
string host = 1;
string port = 2;
}
message Mongo {
string coll = 1;
string options = 2;
string database = 3;
string username = 4;
string password = 5;
string auth_source = 6;
uint64 min_pool_size = 7;
uint64 max_pool_size = 8;
int64 socket_timeout_ms = 9;
int64 connect_timeout_ms = 10;
bool is_zap = 11;
repeated MongoHost hosts = 12;
}
Database database = 1;
Redis redis = 2;
repeated Database database_list = 3;
repeated Redis redis_list = 4;
Mongo mongo = 5;
}
// AdminBackend contains settings for the administration HTTP transport.
message AdminBackend {
string router_prefix = 1;
JWT jwt = 2;
Captcha captcha = 3;
Local local = 4;
Email email = 5;
Storage storage = 6;
// ConfigPath is populated by the entrypoint and is not required in YAML.
string config_path = 7;
Media media = 8;
repeated Disk disk_list = 9;
System system = 10;
Zap zap = 11;
CORS cors = 12;
App app = 13;
message JWT {
string signing_key = 1;
google.protobuf.Duration expires_time = 2;
google.protobuf.Duration buffer_time = 3;
string issuer = 4;
}
message Captcha {
int32 key_long = 1;
int32 img_width = 2;
int32 img_height = 3;
google.protobuf.Duration store_expiration = 4;
}
message Local {
string store_path = 1;
string path_prefix = 2;
}
message Email {
string to = 1;
string from = 2;
string host = 3;
string secret = 4;
string nickname = 5;
int32 port = 6;
bool is_ssl = 7;
bool is_login_auth = 8;
}
message Media {
int32 session_ttl = 1;
int64 max_file_size = 2;
string chunk_dir = 3;
}
message Disk {
string mount_point = 1;
}
message System {
bool use_redis = 1;
bool use_multipoint = 2;
bool use_strict_auth = 3;
bool disable_auto_migrate = 4;
bool use_mongo = 5;
int32 addr = 6;
int32 iplimit_count = 7;
int32 iplimit_time = 8;
}
message Zap {
string level = 1;
string prefix = 2;
string format = 3;
string director = 4;
string encode_level = 5;
string stacktrace_key = 6;
bool show_line = 7;
bool log_in_console = 8;
int32 retention_day = 9;
bool access_req_body = 10;
bool access_resp_data = 11;
bool access_req_headers = 12;
int32 access_log_max_bytes = 13;
repeated string file_only_modules = 14;
}
message CORS {
string mode = 1;
repeated CORSRule whitelist = 2;
}
message CORSRule {
string allow_origin = 1;
string allow_methods = 2;
string allow_headers = 3;
string expose_headers = 4;
bool allow_credentials = 5;
}
message App {
string node = 1;
string app_id = 2;
string env = 3;
}
message Storage {
string type = 1;
Qiniu qiniu = 2;
ObjectStore aliyun_oss = 3;
ObjectStore huawei_obs = 4;
ObjectStore tencent_cos = 5;
ObjectStore aws_s3 = 6;
ObjectStore cloudflare_r2 = 7;
ObjectStore minio = 8;
}
message Qiniu {
string zone = 1;
string bucket = 2;
string base_url = 3;
string access_key = 4;
string secret_key = 5;
bool use_https = 6;
bool use_cdn_domains = 7;
}
// ObjectStore covers S3-compatible configuration used by public cloud
// providers and self-hosted MinIO.
message ObjectStore {
string endpoint = 1;
string region = 2;
string bucket = 3;
string access_key = 4;
string secret_key = 5;
string base_url = 6;
string path_prefix = 7;
bool use_ssl = 8;
bool force_path_style = 9;
string account_id = 10;
}
}

View File

@ -1,128 +0,0 @@
package conf
import (
"sync"
"sync/atomic"
"google.golang.org/protobuf/proto"
)
// Runtime owns the active immutable configuration snapshot. Callers always
// receive clones so a config file reload cannot race with request handling.
type Runtime struct {
snapshot atomic.Pointer[runtimeSnapshot]
mu sync.RWMutex
nextID uint64
listeners map[uint64]func(*Data, *AdminBackend)
}
type runtimeSnapshot struct {
data *Data
admin *AdminBackend
}
func NewRuntime(data *Data, admin *AdminBackend) *Runtime {
runtime := &Runtime{listeners: make(map[uint64]func(*Data, *AdminBackend))}
runtime.Replace(data, admin)
return runtime
}
func cloneData(value *Data) *Data {
if value == nil {
return nil
}
return proto.Clone(value).(*Data)
}
func cloneAdmin(value *AdminBackend) *AdminBackend {
if value == nil {
return nil
}
return proto.Clone(value).(*AdminBackend)
}
func (r *Runtime) Replace(data *Data, admin *AdminBackend) {
data = cloneData(data)
admin = cloneAdmin(admin)
r.snapshot.Store(&runtimeSnapshot{data: data, admin: admin})
r.mu.RLock()
listeners := make([]func(*Data, *AdminBackend), 0, len(r.listeners))
for _, listener := range r.listeners {
listeners = append(listeners, listener)
}
r.mu.RUnlock()
for _, listener := range listeners {
listener(cloneData(data), cloneAdmin(admin))
}
}
// Subscribe registers a callback invoked after every successful runtime
// configuration replacement. It is used by long-lived clients such as the
// logger and other long-lived clients that must follow runtime config changes.
func (r *Runtime) Subscribe(listener func(*Data, *AdminBackend)) func() {
if listener == nil {
return func() {}
}
r.mu.Lock()
r.nextID++
id := r.nextID
r.listeners[id] = listener
r.mu.Unlock()
return func() {
r.mu.Lock()
delete(r.listeners, id)
r.mu.Unlock()
}
}
func (r *Runtime) Data() *Data {
snapshot := r.snapshot.Load()
if snapshot == nil {
return nil
}
return cloneData(snapshot.data)
}
func (r *Runtime) Admin() *AdminBackend {
snapshot := r.snapshot.Load()
if snapshot == nil {
return nil
}
return cloneAdmin(snapshot.admin)
}
func (r *Runtime) Values() (*Data, *AdminBackend) {
snapshot := r.snapshot.Load()
if snapshot == nil {
return nil, nil
}
return cloneData(snapshot.data), cloneAdmin(snapshot.admin)
}
func (r *Runtime) ConfigPath() string {
snapshot := r.snapshot.Load()
if snapshot == nil || snapshot.admin == nil {
return ""
}
return snapshot.admin.ConfigPath
}
func (r *Runtime) UpdateDatabase(database *Data_Database) {
for {
current := r.snapshot.Load()
var data *Data
var admin *AdminBackend
if current != nil {
data = cloneData(current.data)
admin = cloneAdmin(current.admin)
}
if data == nil {
data = &Data{}
}
data.Database = proto.Clone(database).(*Data_Database)
next := &runtimeSnapshot{data: data, admin: admin}
if r.snapshot.CompareAndSwap(current, next) {
return
}
}
}

View File

@ -1,41 +0,0 @@
package conf
import (
"sync"
"testing"
)
func TestRuntimeSnapshotsAreImmutable(t *testing.T) {
runtime := NewRuntime(&Data{Database: &Data_Database{Name: "first"}}, &AdminBackend{RouterPrefix: "/api", Jwt: &AdminBackend_JWT{SigningKey: "secret"}})
admin := runtime.Admin()
admin.RouterPrefix = "/changed"
admin.Jwt.SigningKey = "changed"
data := runtime.Data()
data.Database.Name = "changed"
if got := runtime.Admin(); got.RouterPrefix != "/api" || got.Jwt.SigningKey != "secret" {
t.Fatalf("admin snapshot mutated: %+v", got)
}
if got := runtime.Data().Database.Name; got != "first" {
t.Fatalf("data snapshot mutated: %s", got)
}
}
func TestRuntimeConcurrentReplaceAndRead(t *testing.T) {
runtime := NewRuntime(&Data{Database: &Data_Database{Name: "initial"}}, &AdminBackend{Jwt: &AdminBackend_JWT{SigningKey: "initial"}})
var wait sync.WaitGroup
for worker := 0; worker < 8; worker++ {
wait.Add(1)
go func() {
defer wait.Done()
for index := 0; index < 500; index++ {
_ = runtime.Admin().Jwt.SigningKey
_ = runtime.Data().Database.Name
}
}()
}
for index := 0; index < 500; index++ {
runtime.Replace(&Data{Database: &Data_Database{Name: "next"}}, &AdminBackend{Jwt: &AdminBackend_JWT{SigningKey: "next"}})
}
wait.Wait()
}

View File

@ -2,7 +2,6 @@ package data
import (
"context"
"encoding/json"
"fmt"
"net"
"os"

View File

@ -6,6 +6,7 @@ import (
"time"
"github.com/fsnotify/fsnotify"
"kra/internal/config"
)
// watchConfig refreshes the in-memory config snapshot on file changes, while
@ -79,10 +80,13 @@ func (d *Data) watchConfig() func() {
logger.Error("reload changed config", "mod", "system", "error", loadErr)
continue
}
if next.Data == nil || next.Admin == nil {
logger.Error("reload changed config: data and admin configuration are required", "mod", "system")
if next.Data == nil {
logger.Error("reload changed config: data configuration is required", "mod", "system")
continue
}
if next.Admin == nil {
next.Admin = &config.Admin{}
}
if current := d.runtime.Admin(); current != nil {
next.Admin.Storage = current.Storage
next.Admin.Email = current.Email

View File

@ -13,15 +13,15 @@ import (
func TestConfigWatcherStopWaitsForDebouncedReload(t *testing.T) {
root := t.TempDir()
configPath := filepath.Join(root, "config.yaml")
config := []byte("data:\n database:\n driver: sqlite\n path: " + filepath.ToSlash(root) + "\nadmin: {}\n")
if err := os.WriteFile(configPath, config, 0o600); err != nil {
rawConfig := []byte("data:\n database:\n driver: sqlite\n path: " + filepath.ToSlash(root) + "\nadmin: {}\n")
if err := os.WriteFile(configPath, rawConfig, 0o600); err != nil {
t.Fatal(err)
}
runtime := config.NewStore(&config.Config{Data: &config.Data{}, Admin: &config.Admin{ConfigPath: configPath}})
started := make(chan struct{})
release := make(chan struct{})
var startOnce sync.Once
stopListener := runtime.Subscribe(func(*config.Data, *config.Admin) {
stopListener := runtime.Subscribe(func(*config.Config) {
startOnce.Do(func() { close(started) })
<-release
})
@ -30,7 +30,7 @@ func TestConfigWatcherStopWaitsForDebouncedReload(t *testing.T) {
stop := data.watchConfig()
// A write event schedules the 100ms debounce reload.
if err := os.WriteFile(configPath, config, 0o600); err != nil {
if err := os.WriteFile(configPath, rawConfig, 0o600); err != nil {
t.Fatal(err)
}
select {

View File

@ -78,7 +78,7 @@ extension:
if err := os.WriteFile(configPath, []byte(original), 0o600); err != nil {
t.Fatal(err)
}
runtime := config.NewStore(&config.Data{}, &config.Admin{ConfigPath: configPath})
runtime := config.NewStore(&config.Config{Data: &config.Data{}, Admin: &config.Admin{ConfigPath: configPath}})
data := &Data{runtime: runtime}
if err := data.persistDatabaseConfig(&config.Database{Driver: "mysql", Host: "db", Port: "3306", User: "root", Password: "secret", Name: "kra", Config: "parseTime=True"}, "new-key"); err != nil {
t.Fatal(err)

View File

@ -12,7 +12,6 @@ import (
dataintegration "kra/internal/data/integration"
"kra/internal/integration/storage"
"google.golang.org/protobuf/encoding/protojson"
"gopkg.in/yaml.v3"
"gorm.io/gorm"
)
@ -59,9 +58,9 @@ func TestStorageIntegrationConfigRoundTrip(t *testing.T) {
Qiniu: &config.Qiniu{
Zone: "ZoneHuadong", Bucket: "qiniu-bucket", AccessKey: "qiniu-key", SecretKey: "qiniu-secret",
},
AliyunOss: &config.ObjectStore{
AliyunOSS: &config.ObjectStore{
Endpoint: "oss-cn-hangzhou.aliyuncs.com", Region: "cn-hangzhou", Bucket: "assets",
AccessKey: "aliyun-key", SecretKey: "aliyun-secret", BaseUrl: "https://cdn.example.com", PathPrefix: "uploads",
AccessKey: "aliyun-key", SecretKey: "aliyun-secret", BaseURL: "https://cdn.example.com", PathPrefix: "uploads",
},
Minio: &config.ObjectStore{Endpoint: "127.0.0.1:9000", Bucket: "local", ForcePathStyle: true},
}
@ -70,7 +69,7 @@ func TestStorageIntegrationConfigRoundTrip(t *testing.T) {
}
email := &config.Email{
To: "ops@example.com", From: "mailer@example.com", Host: "smtp.example.com",
Secret: "smtp-secret", Nickname: "Kra", Port: 465, IsSsl: true,
Secret: "smtp-secret", Nickname: "Kra", Port: 465, IsSSL: true,
}
if err := saveEmailIntegrationConfig(db, email); err != nil {
t.Fatal(err)
@ -135,7 +134,7 @@ func TestResolveStorageIntegrationConfigMigratesLegacyOnlyOnce(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if loaded.Type != "qiniu" || loaded.Qiniu.GetBucket() != "legacy" {
if loaded.Type != "qiniu" || loaded.Qiniu.Bucket != "legacy" {
t.Fatalf("migrated storage = %#v", loaded)
}
@ -147,7 +146,7 @@ func TestResolveStorageIntegrationConfigMigratesLegacyOnlyOnce(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if loaded.Type != "qiniu" || loaded.Qiniu.GetSecretKey() != "legacy-secret" {
if loaded.Type != "qiniu" || loaded.Qiniu.SecretKey != "legacy-secret" {
t.Fatalf("database configuration was replaced by legacy config: %#v", loaded)
}
}
@ -155,7 +154,7 @@ func TestResolveStorageIntegrationConfigMigratesLegacyOnlyOnce(t *testing.T) {
func TestMaskStorageSecretsLeavesUnconfiguredProvidersEmpty(t *testing.T) {
storage := &config.Storage{
Qiniu: &config.Qiniu{},
AliyunOss: &config.ObjectStore{SecretKey: "configured-secret"},
AliyunOSS: &config.ObjectStore{SecretKey: "configured-secret"},
Minio: &config.ObjectStore{},
}
maskStorageSecrets(storage)
@ -169,7 +168,7 @@ func TestMaskStorageSecretsLeavesUnconfiguredProvidersEmpty(t *testing.T) {
func TestResolveEmailIntegrationConfigMigratesLegacyOnlyOnce(t *testing.T) {
db := openIntegrationConfigTestDB(t)
legacy := &config.Email{To: "ops@example.com", From: "old@example.com", Host: "smtp.old.example.com", Secret: "old-secret", Port: 465, IsSsl: true}
legacy := &config.Email{To: "ops@example.com", From: "old@example.com", Host: "smtp.old.example.com", Secret: "old-secret", Port: 465, IsSSL: true}
loaded, err := resolveEmailIntegrationConfig(db, legacy)
if err != nil {
t.Fatal(err)
@ -259,17 +258,9 @@ func TestPersistRuntimeConfigReplacesActiveStorage(t *testing.T) {
nextAdmin.Local = &config.Local{StorePath: newRoot, PathPrefix: "new-files"}
nextAdmin.Email = &config.Email{
To: "ops@example.com", From: "mailer@example.com", Host: "smtp.example.com",
Secret: "runtime-secret", Port: 465, IsSsl: true,
Secret: "runtime-secret", Port: 465, IsSSL: true,
}
dataRaw, err := protojson.Marshal(&config.Data{})
if err != nil {
t.Fatal(err)
}
adminRaw, err := protojson.Marshal(nextAdmin)
if err != nil {
t.Fatal(err)
}
if err = d.PersistRuntimeConfig(context.Background(), dataRaw, adminRaw); err != nil {
if err = d.PersistRuntimeConfig(context.Background(), &config.Config{Data: &config.Data{}, Admin: nextAdmin}); err != nil {
t.Fatal(err)
}

View File

@ -3,7 +3,8 @@ package initialize
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"kra/internal/config"
)
@ -34,17 +35,20 @@ func (r *Repo) SaveConfigurationJSON(ctx context.Context, raw json.RawMessage) e
if err := json.Unmarshal(raw, &value); err != nil {
return err
}
if err := mergeJSON(value.Data, &next.Data); err != nil {
if next.Data == nil {
next.Data = &config.Data{}
}
if next.Admin == nil {
next.Admin = &config.Admin{}
}
if err := mergeDataJSON(value.Data, &next.Data); err != nil {
return err
}
if err := mergeJSON(value.Admin, &next.Admin); err != nil {
if err := mergeAdminJSON(value.Admin, &next.Admin); err != nil {
return err
}
if len(value.Email) > 0 && string(value.Email) != "null" {
if next.Admin == nil {
next.Admin = &config.Admin{}
}
if err := mergeJSON(value.Email, &next.Admin.Email); err != nil {
if err := mergeEmailJSON(value.Email, &next.Admin.Email); err != nil {
return err
}
}
@ -79,13 +83,144 @@ func mergeJSON(raw json.RawMessage, target any) error {
return json.Unmarshal(raw, target)
}
func mergeDataJSON(raw json.RawMessage, target **config.Data) error {
return mergeJSONMap(raw, target, func(values map[string]any) error {
if redis := jsonObject(values["redis"]); redis != nil {
if err := normalizeDuration(redis, "read_timeout", "readTimeout"); err != nil {
return err
}
if err := normalizeDuration(redis, "write_timeout", "writeTimeout"); err != nil {
return err
}
}
if items, ok := values["redis_list"].([]any); ok {
for _, item := range items {
redis := jsonObject(item)
if err := normalizeDuration(redis, "read_timeout", "readTimeout"); err != nil {
return err
}
if err := normalizeDuration(redis, "write_timeout", "writeTimeout"); err != nil {
return err
}
}
}
return nil
})
}
func mergeAdminJSON(raw json.RawMessage, target **config.Admin) error {
return mergeJSONMap(raw, target, func(values map[string]any) error {
moveJSONKey(values, "routerPrefix", "router_prefix")
if jwt := jsonObject(values["jwt"]); jwt != nil {
moveJSONKey(jwt, "signingKey", "signing_key")
moveJSONKey(jwt, "expiresTime", "expires_time")
moveJSONKey(jwt, "bufferTime", "buffer_time")
if err := normalizeDuration(jwt, "expires_time", "expiresTime"); err != nil {
return err
}
if err := normalizeDuration(jwt, "buffer_time", "bufferTime"); err != nil {
return err
}
}
if captcha := jsonObject(values["captcha"]); captcha != nil {
moveJSONKey(captcha, "keyLong", "key_long")
moveJSONKey(captcha, "imgWidth", "img_width")
moveJSONKey(captcha, "imgHeight", "img_height")
moveJSONKey(captcha, "storeExpiration", "store_expiration")
return normalizeDuration(captcha, "store_expiration", "storeExpiration")
}
if local := jsonObject(values["local"]); local != nil {
moveJSONKey(local, "storePath", "store_path")
moveJSONKey(local, "pathPrefix", "path_prefix")
}
if media := jsonObject(values["media"]); media != nil {
moveJSONKey(media, "sessionTtl", "session_ttl")
moveJSONKey(media, "maxFileSize", "max_file_size")
moveJSONKey(media, "chunkDir", "chunk_dir")
}
if system := jsonObject(values["system"]); system != nil {
moveJSONKey(system, "useRedis", "use_redis")
moveJSONKey(system, "useMultipoint", "use_multipoint")
moveJSONKey(system, "useStrictAuth", "use_strict_auth")
moveJSONKey(system, "disableAutoMigrate", "disable_auto_migrate")
moveJSONKey(system, "useMongo", "use_mongo")
moveJSONKey(system, "iplimitCount", "iplimit_count")
moveJSONKey(system, "iplimitTime", "iplimit_time")
}
return nil
})
}
func mergeEmailJSON(raw json.RawMessage, target **config.Email) error {
return mergeJSONMap(raw, target, func(values map[string]any) error {
moveJSONKey(values, "is-ssl", "is_ssl")
moveJSONKey(values, "is-loginauth", "is_login_auth")
return nil
})
}
func mergeJSONMap(raw json.RawMessage, target any, transform func(map[string]any) error) error {
if len(raw) == 0 || string(raw) == "null" {
return nil
}
var values map[string]any
if err := json.Unmarshal(raw, &values); err != nil {
return err
}
if transform != nil {
if err := transform(values); err != nil {
return err
}
}
normalized, err := json.Marshal(values)
if err != nil {
return err
}
return mergeJSON(normalized, target)
}
func jsonObject(value any) map[string]any {
result, _ := value.(map[string]any)
return result
}
func normalizeDuration(values map[string]any, keys ...string) error {
if values == nil {
return nil
}
for _, key := range keys {
raw, ok := values[key]
if !ok {
continue
}
text, ok := raw.(string)
if !ok {
return nil
}
value, err := time.ParseDuration(text)
if err != nil {
return fmt.Errorf("invalid duration %q: %w", text, err)
}
values[key] = int64(value)
return nil
}
return nil
}
func moveJSONKey(values map[string]any, oldKey, newKey string) {
if value, ok := values[oldKey]; ok {
values[newKey] = value
delete(values, oldKey)
}
}
func managementConfig(value *config.Config) map[string]any {
result := map[string]any{"data": map[string]any{}, "admin": map[string]any{}, "email": map[string]any{}}
if value == nil {
return result
}
if value.Data != nil {
result["data"] = value.Data
result["data"] = managementData(value.Data)
}
if value.Admin != nil {
admin := value.Admin
@ -113,6 +248,36 @@ func managementConfig(value *config.Config) map[string]any {
return result
}
func managementData(value *config.Data) any {
if value == nil {
return map[string]any{}
}
raw, err := json.Marshal(value)
if err != nil {
return value
}
var result map[string]any
if json.Unmarshal(raw, &result) != nil {
return value
}
if redis := jsonObject(result["redis"]); redis != nil {
redis["read_timeout"] = value.Redis.ReadTimeout.String()
redis["write_timeout"] = value.Redis.WriteTimeout.String()
}
if items, ok := result["redis_list"].([]any); ok {
for index, item := range items {
if index >= len(value.RedisList) || value.RedisList[index] == nil {
continue
}
if redis := jsonObject(item); redis != nil {
redis["read_timeout"] = value.RedisList[index].ReadTimeout.String()
redis["write_timeout"] = value.RedisList[index].WriteTimeout.String()
}
}
}
return result
}
func managementJWT(value *config.JWT) any {
if value == nil {
return map[string]any{}
@ -336,14 +501,11 @@ func refreshDatabaseSource(database *config.Database) error {
if database == nil {
return nil
}
if database.Driver == "sqlite" && database.Path == "" {
return errors.New("sqlite database path is required")
}
if database.Driver == "sqlite" {
database.Source = database.Path
} else {
hasStructuredConfig := database.Host != "" || database.Port != "" || database.User != "" || database.Password != "" || database.Name != "" || database.Config != "" || database.Path != ""
if hasStructuredConfig {
// Driver-specific DSN construction remains in data. Clearing Source
// makes the data backend rebuild it from structured values.
// makes the data backend rebuild it from structured values, while a
// standalone DSN is preserved when no structured fields are supplied.
database.Source = ""
}
return nil

View File

@ -54,10 +54,10 @@ func TestConfigurationJSONMasksSecretsWithoutMutatingConfig(t *testing.T) {
func TestSaveConfigurationJSONMergesPartialValuesAndPreservesSecrets(t *testing.T) {
backend := &configurationBackend{value: &config.Config{
Data: &config.Data{Database: &config.Database{Driver: "mysql", Host: "old", Password: "database-secret", Source: "old-dsn"}, Redis: &config.Redis{Name: "main", Password: "redis-secret"}},
Admin: &config.Admin{ConfigPath: "config.yaml", JWT: &config.JWT{SigningKey: "jwt-secret", Issuer: "old"}, Email: &config.Email{Host: "old.smtp", Secret: "email-secret"}},
Data: &config.Data{Database: &config.Database{Driver: "mysql", Host: "old", Password: "database-secret", Source: "old-dsn"}, Redis: &config.Redis{Name: "main", Password: "redis-secret", ReadTimeout: time.Second}},
Admin: &config.Admin{ConfigPath: "config.yaml", JWT: &config.JWT{SigningKey: "jwt-secret", Issuer: "old", ExpiresTime: time.Hour}, Captcha: &config.Captcha{StoreExpiration: time.Minute}, Email: &config.Email{Host: "old.smtp", Secret: "email-secret", IsSSL: true}},
}}
raw := json.RawMessage(`{"data":{"database":{"host":"new","password":"******"}},"admin":{"jwt":{"issuer":"new","signing_key":"******"}},"email":{"host":"new.smtp","secret":"******"}}`)
raw := json.RawMessage(`{"data":{"database":{"host":"new","password":"******"},"redis":{"read_timeout":"250ms"}},"admin":{"jwt":{"issuer":"new","signingKey":"******","expiresTime":"48h"},"captcha":{"storeExpiration":"5m"}},"email":{"host":"new.smtp","secret":"******","is-ssl":false}}`)
if err := (&Repo{backend: backend}).SaveConfigurationJSON(context.Background(), raw); err != nil {
t.Fatal(err)
}
@ -68,7 +68,23 @@ func TestSaveConfigurationJSONMergesPartialValuesAndPreservesSecrets(t *testing.
if got.Data.Database.Password != "database-secret" || got.Admin.JWT.SigningKey != "jwt-secret" || got.Admin.Email.Secret != "email-secret" {
t.Fatal("masked secrets were not preserved")
}
if got.Data.Redis.ReadTimeout != 250*time.Millisecond || got.Admin.JWT.ExpiresTime != 48*time.Hour || got.Admin.Captcha.StoreExpiration != 5*time.Minute || got.Admin.Email.IsSSL {
t.Fatalf("management values were not decoded: redis=%s jwt=%s captcha=%s ssl=%t", got.Data.Redis.ReadTimeout, got.Admin.JWT.ExpiresTime, got.Admin.Captcha.StoreExpiration, got.Admin.Email.IsSSL)
}
if got.Admin.ConfigPath != "config.yaml" {
t.Fatalf("config path = %q", got.Admin.ConfigPath)
}
}
func TestSaveConfigurationJSONPreservesStandaloneDSN(t *testing.T) {
backend := &configurationBackend{value: &config.Config{
Data: &config.Data{Database: &config.Database{Driver: "mysql", Source: "user:secret@tcp(database.example:3306)/kra"}},
Admin: &config.Admin{},
}}
if err := (&Repo{backend: backend}).SaveConfigurationJSON(context.Background(), json.RawMessage(`{"admin":{"routerPrefix":"/api"}}`)); err != nil {
t.Fatal(err)
}
if got := backend.persisted.Data.Database.Source; got != "user:secret@tcp(database.example:3306)/kra" {
t.Fatalf("standalone DSN = %q", got)
}
}

224
usage.txt
View File

@ -1,224 +0,0 @@
cmd\main.go:13: "kra/internal/conf"
cmd\main.go:147: runtime := conf.NewRuntime(bc.Data, bc.Admin)
cmd\main.go:148: unsubscribeLogger := runtime.Subscribe(func(_ *conf.Data, admin *conf.AdminBackend) {
cmd\main_test.go:6: "kra/internal/conf"
cmd\wire_gen.go:16: "kra/internal/conf"
internal\app\runtime.go:30: return module.NewRuntime(contributions.Routes...)
cmd\wire.go:14: "kra/internal/conf"
pkg\module\module_test.go:54: runtime := NewRuntime(nil, stub)
pkg\mq\mqtt.go:102: return waitToken(ctx, client.Subscribe(topic, qos, func(_ paho.Client, msg paho.Message) {
pkg\module\module.go:84:func NewRuntime(routes ...RouteRegistrar) *Runtime {
internal\config\runtime_test.go:63: runtime := NewRuntime(&Config{Admin: &Admin{JWT: &JWT{SigningKey: "before"}}})
internal\config\runtime_test.go:65: stop := runtime.Subscribe(func(value *Config) {
internal\config\runtime_test.go:69: runtime.Replace(&Config{Admin: &Admin{JWT: &JWT{SigningKey: "after"}}})
internal\config\runtime_test.go:72: value := runtime.Snapshot()
internal\config\runtime_test.go:82: runtime := NewRuntime(&Config{Data: &Data{Database: &Database{Name: "initial"}}})
internal\config\runtime_test.go:89: _ = runtime.Snapshot()
internal\config\runtime_test.go:94: runtime.Replace(&Config{Data: &Data{Database: &Database{Name: "next"}}})
internal\config\runtime_test.go:111: value := runtime.Snapshot()
internal\config\runtime_test.go:117: t.Fatalf("watcher did not reload configuration: %#v", runtime.Snapshot())
internal\config\runtime.go:56:func NewRuntime(config *Config) *Runtime {
internal\config\runtime.go:61: runtime.Replace(config)
internal\config\runtime.go:71: runtime := NewRuntime(config)
internal\config\runtime.go:87: config := r.Snapshot()
internal\config\runtime.go:95: config := r.Snapshot()
internal\config\runtime.go:103: config := r.Snapshot()
internal\config\runtime.go:111: config := r.Snapshot()
internal\config\runtime.go:122: config := r.Snapshot()
internal\config\runtime.go:155: if path := r.ConfigPath(); path != "" {
internal\config\runtime.go:200: path := r.ConfigPath()
internal\config\runtime.go:268: r.Replace(config)
internal\global\resources_test.go:61: resources := registry.Snapshot()
internal\global\resources_test.go:79: registry.Replace(Resources{
internal\global\resources_test.go:84: resources := registry.Snapshot()
internal\global\resources_test.go:124: registry.Replace(Resources{Redis: redisClient, Storage: storage, MQ: mq, WebSocket: webSocket, Scheduler: scheduler})
internal\global\resources_test.go:125: resources := registry.Snapshot()
internal\global\resources_test.go:144: _ = registry.Snapshot()
internal\global\resources.go:80:func (r *ResourceRegistry) Logger() *slog.Logger { return r.Snapshot().Logger }
internal\global\resources.go:86:func (r *ResourceRegistry) DB() *gorm.DB { return r.Snapshot().DB }
internal\global\resources.go:98: return r.Snapshot().NamedDBs[name]
internal\global\resources.go:106:func (r *ResourceRegistry) Redis() redis.UniversalClient { return r.Snapshot().Redis }
internal\global\resources.go:118: return r.Snapshot().NamedRedis[name]
internal\global\resources.go:126:func (r *ResourceRegistry) Mongo() *mongo.Client { return r.Snapshot().Mongo }
internal\global\resources.go:132:func (r *ResourceRegistry) Storage() FileStorage { return r.Snapshot().Storage }
internal\global\resources.go:138:func (r *ResourceRegistry) MQ() platformmq.Registry { return r.Snapshot().MQ }
internal\global\resources.go:144:func (r *ResourceRegistry) WebSocket() platformws.Hub { return r.Snapshot().WebSocket }
internal\global\resources.go:150:func (r *ResourceRegistry) Scheduler() Scheduler { return r.Snapshot().Scheduler }
internal\global\resources.go:241:func ResourceSnapshot() Resources { return defaultResources.Snapshot() }
internal\global\resources.go:243:func ReplaceResources(resources Resources) { defaultResources.Replace(resources) }
internal\conf\runtime_test.go:9: runtime := NewRuntime(&Data{Database: &Data_Database{Name: "first"}}, &AdminBackend{RouterPrefix: "/api", Jwt: &AdminBackend_JWT{SigningKey: "secret"}})
internal\conf\runtime_test.go:25: runtime := NewRuntime(&Data{Database: &Data_Database{Name: "initial"}}, &AdminBackend{Jwt: &AdminBackend_JWT{SigningKey: "initial"}})
internal\conf\runtime_test.go:38: runtime.Replace(&Data{Database: &Data_Database{Name: "next"}}, &AdminBackend{Jwt: &AdminBackend_JWT{SigningKey: "next"}})
internal\conf\runtime.go:24:func NewRuntime(data *Data, admin *AdminBackend) *Runtime {
internal\conf\runtime.go:26: runtime.Replace(data, admin)
internal\conf\conf.pb.go:2216: " \x01(\tR\taccountIdB\x18Z\x16kra/internal/conf;confb\x06proto3"
internal\biz\task\task.go:236: return uc.runtime.Subscribe(userID)
internal\data\database.go:22: "kra/internal/config"
internal\data\data.go:14: "kra/internal/config"
internal\data\data.go:52: runtime *config.Runtime
internal\data\data.go:79:func (d *Data) Runtime() *config.Runtime {
internal\data\data.go:164:func NewData(runtime *config.Runtime, appLogger *slog.Logger, storageManager *storage.Reloadable, catalog module.Catalog) (*Data, func(), error) {
internal\data\data.go:256: runtime.Replace(c, admin)
internal\data\data.go:265: storageManager.Replace(activeStorage)
internal\data\data.go:327: d.runtime.UpdateDatabase(config)
internal\data\initialization_backend_test.go:10: "kra/internal/config"
internal\data\initialization_backend_test.go:81: runtime := config.NewRuntime(&config.Data{}, &config.Admin{ConfigPath: configPath})
internal\data\config_watch_test.go:10: "kra/internal/config"
internal\data\config_watch_test.go:20: runtime := config.NewRuntime(&config.Data{}, &config.Admin{ConfigPath: configPath})
internal\data\config_watch_test.go:24: stopListener := runtime.Subscribe(func(*config.Data, *config.Admin) {
internal\data\initialization_backend.go:9: "kra/internal/config"
internal\data\initialization_backend.go:18:func (d *Data) RuntimeValues() (*config.Data, *config.Admin) { return d.runtime.Values() }
internal\data\initialization_backend.go:61: currentData, currentAdmin := d.runtime.Values()
internal\data\initialization_backend.go:88: d.runtime.Replace(currentData, next)
internal\data\initialization_backend.go:90: d.storage.Replace(candidateStorage)
internal\data\initialization_backend.go:95: currentData, currentAdmin := d.runtime.Values()
internal\data\initialization_backend.go:125: d.runtime.Replace(nextData, nextAdmin)
internal\data\initialization_backend.go:127: d.storage.Replace(candidateStorage)
internal\data\initialization_backend.go:218: currentData, currentAdmin := d.runtime.Values()
internal\data\initialization_backend.go:228: d.runtime.Replace(currentData, currentAdmin)
internal\data\initialization_backend.go:230: d.integrations.Replace(integrationConfigs)
internal\data\integration_config.go:10: "kra/internal/config"
internal\data\integration_config_test.go:11: "kra/internal/config"
internal\data\integration_config_test.go:252: runtime: config.NewRuntime(&config.Data{}, currentAdmin),
internal\data\config_helpers.go:4: "kra/internal/config"
internal\data\config_store.go:12: "kra/internal/config"
internal\data\config_store.go:155: dataConfig, adminConfig := d.runtime.Values()
internal\data\config_store.go:214: configPath := d.runtime.ConfigPath()
internal\data\config_store.go:262: configPath := d.runtime.ConfigPath()
internal\data\config_store.go:313: configPath := d.runtime.ConfigPath()
internal\data\config_store.go:431: d.runtime.Replace(next.Data, next.Admin)
internal\data\config_store.go:433: d.integrations.Replace(integrationConfigs)
internal\data\config_store.go:436: d.storage.Replace(candidateStorage)
internal\data\config_watch.go:16: configPath := d.runtime.ConfigPath()
internal\data\config_watch.go:91: d.runtime.Replace(next.Data, next.Admin)
internal\server\gin.go:9: "kra/internal/config"
internal\server\gin.go:23:func NewGinEngine(runtime *config.Runtime, access *service.AccessControlService, handlers *handler.Set, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string) *gin.Engine {
internal\server\gin.go:24: return NewGinEngineWithRuntime(runtime, access, auth, security, audit, logger, version, platformmodule.NewRuntime(router.NewRoutes(handlers)), nil)
internal\server\gin.go:27:func NewGinEngineWithRuntime(runtime *config.Runtime, access *service.AccessControlService, auth middleware.TokenAuthenticator, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string, routes *platformmodule.Runtime, ws *websocket.Server) *gin.Engine {
internal\server\gin.go:29: runtime = config.NewRuntime(nil)
internal\server\gin.go:39: snapshot := runtime.Snapshot()
internal\server\gin_test.go:14: "kra/internal/config"
internal\server\gin_test.go:35: engine := NewGinEngine(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
internal\server\gin_test.go:69: engine := NewGinEngine(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
internal\server\gin_test.go:89: engine := NewGinEngine(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, logger, "test")
internal\server\gin_test.go:100: engine := NewGinEngine(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
internal\server\gin_test.go:116: engine := NewGinEngine(config.NewRuntime(&config.Config{Admin: &config.Admin{RouterPrefix: "/admin"}}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
internal\server\gin_test.go:131: engine := NewGinEngine(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "v1.0.0")
internal\server\gin_test.go:140: engine := NewGinEngine(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
internal\server\gin_test.go:154: engine := NewGinEngine(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), nil, emptyHandlers(), nil, nil, nil, nil, "test")
internal\server\gin_test.go:169: runtime := config.NewRuntime(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &config.Storage{Type: "local"}}})
internal\server\gin_test.go:193: runtime := config.NewRuntime(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}}})
internal\server\gin_test.go:216: runtime := config.NewRuntime(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: root, PathPrefix: "uploads/file"}, Storage: &config.Storage{Type: "local"}}})
internal\server\gin_test.go:218: runtime.Replace(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: root, PathPrefix: "files"}, Storage: &config.Storage{Type: "local"}}})
internal\server\gin_test.go:233: runtime := config.NewRuntime(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: t.TempDir(), PathPrefix: "api"}, Storage: &config.Storage{Type: "local"}}})
internal\server\gin_test.go:243: runtime := config.NewRuntime(&config.Config{Admin: &config.Admin{Local: &config.Local{StorePath: t.TempDir(), PathPrefix: "uploads/file"}, Storage: &config.Storage{Type: "s3"}}})
internal\data\integration_runtime.go:19: d.integrations.Replace(configs)
internal\data\mongo.go:9: "kra/internal/config"
internal\worker\task_scheduler_test.go:10: first := scheduler.Subscribe(1)
internal\worker\task_scheduler_test.go:11: second := scheduler.Subscribe(1)
internal\worker\task_scheduler_test.go:12: third := scheduler.Subscribe(2)
internal\worker\task_scheduler_test.go:34: channels = append(channels, scheduler.Subscribe(7))
internal\worker\task_scheduler_test.go:60: ch := scheduler.Subscribe(9)
internal\worker\task_registry.go:10: "kra/internal/config"
internal\worker\task_registry.go:20: runtime *config.Runtime
internal\worker\task_registry.go:23:func NewTaskMethods(tasks *taskbiz.TaskUsecase, maintenance *system.MaintenanceUsecase, media *system.MediaUsecase, runtime *config.Runtime) *TaskMethods {
internal\worker\task_registry.go:58: if snapshot := methods.runtime.Snapshot(); snapshot != nil {
internal\data\payment\payment.go:411: normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
internal\server\middleware\access_log.go:18: "kra/internal/config"
internal\server\middleware\access_log.go:28:func AccessLog(runtime *config.Runtime, logger *slog.Logger, version string) gin.HandlerFunc {
internal\server\middleware\access_log.go:36: if snapshot := runtime.Snapshot(); snapshot != nil {
internal\server\staticfiles\staticfiles.go:12: "kra/internal/config"
internal\server\staticfiles\staticfiles.go:19:func Register(engine *gin.Engine, runtime *config.Runtime) {
internal\server\staticfiles\staticfiles.go:39:func Serve(c *gin.Context, runtime *config.Runtime) bool {
internal\server\staticfiles\staticfiles.go:48:func localConfig(runtime *config.Runtime) *config.Local {
internal\server\staticfiles\staticfiles.go:52: snapshot := runtime.Snapshot()
internal\server\staticfiles\staticfiles.go:76:func serveAt(c *gin.Context, runtime *config.Runtime, prefix string) bool {
internal\server\middleware\access.go:7: "kra/internal/config"
internal\server\middleware\access.go:18:func AccessControl(runtime *config.Runtime, access accessController) gin.HandlerFunc {
internal\server\middleware\access.go:33: if snapshot := runtime.Snapshot(); snapshot != nil && snapshot.Admin != nil {
internal\server\websocket_auth_test.go:12: "kra/internal/config"
internal\server\websocket_auth_test.go:47: engine := NewGinEngineWithRuntime(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), nil, auth, nil, nil, nil, "test", nil, ws)
internal\integration\websocket\server.go:40: s.stop = store.Subscribe("websocket", ProviderMelody, func(config runtimeconfig.Config) { s.apply(config) })
internal\server\middleware\audit.go:15: "kra/internal/config"
internal\server\middleware\audit.go:25:func OperationAudit(runtime *config.Runtime, recorder *service.AuditRecorder) gin.HandlerFunc {
internal\server\middleware\audit.go:42: if snapshot := runtime.Snapshot(); snapshot != nil && snapshot.Admin != nil && snapshot.Admin.Zap != nil && snapshot.Admin.Zap.AccessLogMaxBytes > 0 {
internal\server\middleware\cors_test.go:8: "kra/internal/config"
internal\server\middleware\cors_test.go:16: engine.Use(CORS(config.NewRuntime(&config.Config{Admin: admin})))
internal\server\middleware\cors.go:7: "kra/internal/config"
internal\server\middleware\cors.go:20:func CORS(runtime *config.Runtime) gin.HandlerFunc {
internal\server\middleware\cors.go:26: snapshot := runtime.Snapshot()
internal\server\middleware\access_test.go:12: "kra/internal/config"
internal\server\middleware\access_test.go:39: engine.Use(AccessControl(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), &accessControllerStub{scopeErr: errors.New("database unavailable")}))
internal\server\middleware\access_log_test.go:12: "kra/internal/config"
internal\server\middleware\access_log_test.go:21: engine.Use(AccessLog(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), nil, "test"))
internal\server\middleware\access_log_test.go:37: engine.Use(AccessLog(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), nil, "test"))
internal\server\middleware\access_log_test.go:54: engine.Use(AccessLog(config.NewRuntime(&config.Config{Admin: &config.Admin{}}), nil, "test"))
internal\server\middleware\access_log_test.go:73: runtime := config.NewRuntime(&config.Config{Admin: &config.Admin{Zap: &config.Zap{AccessReqBody: true, AccessReqHeaders: true, AccessRespData: true}}})
internal\server\middleware\access_log_test.go:104: runtime := config.NewRuntime(&config.Config{Admin: &config.Admin{Zap: &config.Zap{AccessReqBody: true, AccessReqHeaders: true, AccessRespData: true}}})
internal\server\middleware\access_log_test.go:130: runtime := config.NewRuntime(&config.Config{Admin: &config.Admin{}})
internal\integration\email\email_test.go:7: "kra/internal/config"
internal\integration\email\email_test.go:11: runtime := config.NewRuntime(&config.Data{}, &config.Admin{Email: &config.Email{To: " "}})
internal\integration\email\email_test.go:19: runtime := config.NewRuntime(&config.Data{}, &config.Admin{Email: &config.Email{Host: "smtp.example.com", From: "from@example.com", Secret: "secret", Port: 25}})
internal\integration\email\email.go:16: "kra/internal/config"
internal\integration\email\email.go:19:type emailRepo struct{ runtime *config.Runtime }
internal\integration\email\email.go:21:func NewEmailRepo(runtime *config.Runtime) system.EmailRepo {
internal\integration\email\email.go:64: return strings.NewReplacer("\r", "", "\n", "").Replace(value)
internal\service\task\task.go:81:func (s *TaskService) Subscribe(userID uint) chan []byte { return s.uc.Subscribe(userID) }
internal\server\handler\task.go:138: events := h.service.Subscribe(claims.ID)
internal\initialize\configuration_test.go:7: "kra/internal/conf"
internal\integration\storage\tencent_storage.go:14: "kra/internal/config"
internal\initialize\configuration.go:7: "kra/internal/conf"
internal\integration\storage\s3_storage.go:12: "kra/internal/config"
internal\integration\storage\reloadable_test.go:68: reloadable.Replace(&emptyStorage{})
internal\initialize\compatibility.go:9: "kra/internal/conf"
internal\initialize\backend.go:7: "kra/internal/conf"
internal\data\system\api_policy_test.go:9: "kra/internal/config"
internal\data\system\api_policy_test.go:23: runtime: config.NewRuntime(
internal\integration\storage\reloadable.go:10: "kra/internal/config"
internal\integration\storage\reloadable.go:18:func NewFileStorage(runtime *config.Runtime) (*Reloadable, error) {
internal\integration\storage\qiniu_storage.go:14: "kra/internal/config"
internal\integration\storage\local.go:14: "kra/internal/config"
internal\integration\storage\huawei_storage.go:11: "kra/internal/config"
internal\data\system\authority_test.go:9: "kra/internal/config"
internal\data\system\authority_test.go:17: currentData, _ := data.runtime.Values()
internal\data\system\authority_test.go:18: data.runtime.Replace(currentData, &config.Admin{System: &config.System{UseStrictAuth: true}})
internal\integration\storage\aws_storage.go:11: "kra/internal/config"
internal\integration\storage\aliyun_storage.go:11: "kra/internal/config"
internal\integration\payment\alipay.go:439: normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
internal\integration\runtimeconfig\store_test.go:12: stop := store.Subscribe("mq", "rabbitmq", func(config Config) { updates <- config })
internal\integration\runtimeconfig\store_test.go:36: stop := store.Subscribe("mq", "rabbitmq", func(config Config) { updates <- config })
internal\integration\runtimeconfig\store_test.go:47: store.Replace([]Config{config})
internal\integration\runtimeconfig\store_test.go:56: store.Replace([]Config{changed})
internal\integration\runtimeconfig\store_test.go:70: stop := store.Subscribe("mq", "rabbitmq", func(config Config) { updates <- config })
internal\integration\payment\allinpay.go:210: normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
internal\integration\mq\emqx.go:71: store.Subscribe("mq", ProviderEMQX, func(config runtimeconfig.Config) { r.apply(ProviderEMQX, config) }),
internal\integration\mq\emqx.go:72: store.Subscribe("mq", ProviderRabbitMQ, func(config runtimeconfig.Config) { r.apply(ProviderRabbitMQ, config) }),
internal\integration\mq\emqx.go:362: if err := client.Subscribe(context.Background(), topic, qos, r.dispatcher(provider, topic)); err != nil {
internal\integration\mq\emqx.go:445: if err := client.Subscribe(context.Background(), topic, qos, r.dispatcher(provider, topic)); err != nil {
internal\integration\payment\lakala.go:74: return strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(method)))
internal\integration\mq\emqx_test.go:57: if err := r.Subscribe(context.Background(), "orders/+/paid", platformmq.AtLeastOnce, handler); err != nil {
internal\integration\mq\emqx_test.go:204: err := r.Subscribe(context.Background(), "events.offline", platformmq.AtLeastOnce, func(context.Context, platformmq.Message) {})
internal\integration\payment\douyin.go:113: normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
internal\data\system\log_file_test.go:11: "kra/internal/config"
internal\data\system\log_file_test.go:25: runtime := config.NewRuntime(nil, &config.Admin{Zap: &config.Zap{Director: root}})
internal\data\system\log_file_test.go:44: runtime := config.NewRuntime(nil, &config.Admin{Zap: &config.Zap{Director: t.TempDir()}})
internal\integration\payment\saobei.go:92: normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
internal\data\system\menu_test.go:9: "kra/internal/config"
internal\data\system\menu_test.go:23: runtime: config.NewRuntime(&config.Data{Database: &config.Database{Driver: "sqlite"}}, &config.Admin{}),
internal\integration\payment\wechat_v3.go:123: return strings.NewReplacer(".", "", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
internal\integration\payment\wechat_v2.go:189: normalized := strings.NewReplacer(".", "_", "-", "_", " ", "_").Replace(strings.ToLower(strings.TrimSpace(value)))
internal\data\system\permission_test.go:8: "kra/internal/config"
internal\data\system\permission_test.go:19: data := &Data{gormDB: newReloadableDB(db, nil), runtime: config.NewRuntime(&config.Data{Database: &config.Database{Driver: "sqlite"}}, &config.Admin{})}
internal\data\system\permission_test.go:39: data := &Data{gormDB: newReloadableDB(db, nil), runtime: config.NewRuntime(&config.Data{Database: &config.Database{Driver: "sqlite"}}, &config.Admin{})}
internal\data\system\permission_test.go:70: data := &Data{gormDB: newReloadableDB(db, nil), runtime: config.NewRuntime(&config.Data{Database: &config.Database{Driver: "sqlite"}}, &config.Admin{})}
internal\data\system\provider.go:4: "kra/internal/config"
internal\data\system\provider.go:15: Runtime() *config.Runtime
internal\data\system\runtime.go:10: "kra/internal/config"
internal\data\system\runtime.go:16:type runtimeSettings struct{ runtime *config.Runtime }
internal\data\system\runtime.go:18:func NewRuntimeSettings(runtime *config.Runtime) system.RuntimeSettings {
internal\data\system\testing_support_test.go:10: "kra/internal/config"
internal\data\system\testing_support_test.go:19: runtime *config.Runtime
internal\data\system\testing_support_test.go:32:func (d *Data) Runtime() *config.Runtime {
internal\data\system\transactions_test.go:9: "kra/internal/config"
internal\data\system\transactions_test.go:14: name := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name())
internal\data\system\transactions_test.go:22: data := &Data{gormDB: newReloadableDB(db, nil), redis: newReloadableRedis(nil), runtime: config.NewRuntime(&config.Data{Database: &config.Database{Driver: "sqlite"}}, &config.Admin{})}