mirror of
https://github.com/tnb-labs/panel.git
synced 2026-09-21 05:11:14 +08:00
依赖注入:
- 移除 google/wire(含 wire.go/wire_gen.go 与全部 ProviderSet),
改用 samber/do v2 单注入器 + 双入口惰性构建
- 贡献模型替代命令式注册:路由 routes:、命令 commands:、
任务 jobs: 前缀经 internal/registry 收集与校验
- 构造函数统一 func NewXxx(i do.Injector) (T, error)
三层架构:
- 补全用例层:每个 biz.XxxRepo 配 XxxUsecase,
service/command/job 表现层只依赖用例,不再直接引用仓储
路由与文档:
- route/http.go 按域拆为声明式 Endpoint 贡献,
Endpoint 承载登录白名单与端点限流语义
- 调试模式下提供 OpenAPI 3.1 文档:/openapi.json 与 /docs(Scalar),
从 validate 标签生成
目录与依赖:
- internal/http/{middleware,request,rule} 拍平至 internal/*
- CLI 命令拆至 internal/command
- 日志轮转 timberjack 换为 libtnb/logrotate
- 升级 validator/cron/sessions/gormstore/sqlite/securecookie
- 数据库迁移保持 gormigrate 不变
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
334 lines
9.9 KiB
Go
334 lines
9.9 KiB
Go
package clickhouse
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/go-chi/chi/v5"
|
||
"github.com/leonelquinteros/gotext"
|
||
"github.com/samber/do/v2"
|
||
"github.com/spf13/cast"
|
||
"go.yaml.in/yaml/v4"
|
||
"resty.dev/v3"
|
||
|
||
"github.com/acepanel/panel/v3/internal/app"
|
||
"github.com/acepanel/panel/v3/internal/biz"
|
||
"github.com/acepanel/panel/v3/internal/service"
|
||
"github.com/acepanel/panel/v3/pkg/io"
|
||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||
"github.com/acepanel/panel/v3/pkg/types"
|
||
)
|
||
|
||
type App struct {
|
||
t *gotext.Locale
|
||
settingRepo biz.SettingRepo
|
||
databaseServerRepo biz.DatabaseServerRepo
|
||
}
|
||
|
||
func NewApp(i do.Injector) (*App, error) {
|
||
return &App{
|
||
t: do.MustInvoke[*gotext.Locale](i),
|
||
settingRepo: do.MustInvoke[biz.SettingRepo](i),
|
||
databaseServerRepo: do.MustInvoke[biz.DatabaseServerRepo](i),
|
||
}, nil
|
||
}
|
||
|
||
func (s *App) Route(r chi.Router) {
|
||
r.Get("/load", s.Load)
|
||
r.Get("/config", s.GetConfig)
|
||
r.Post("/config", s.UpdateConfig)
|
||
r.Get("/config_tune", s.GetConfigTune)
|
||
r.Post("/config_tune", s.UpdateConfigTune)
|
||
r.Get("/default_password", s.GetDefaultPassword)
|
||
r.Post("/default_password", s.SetDefaultPassword)
|
||
}
|
||
|
||
func (s *App) Status() string {
|
||
ok, _ := systemctl.Status("clickhouse-server")
|
||
return types.AggregateAppStatus(ok)
|
||
}
|
||
|
||
// Load 获取 ClickHouse 运行状态
|
||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||
status, _ := systemctl.Status("clickhouse-server")
|
||
if !status {
|
||
service.Success(w, []types.NV{})
|
||
return
|
||
}
|
||
|
||
password, _ := s.settingRepo.Get(biz.SettingKeyClickHouseDefaultPassword)
|
||
port := s.getPort()
|
||
|
||
client := resty.New().SetTimeout(10 * time.Second)
|
||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||
|
||
// 获取版本
|
||
versionResp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+version()&user=default&password=%s", port, password))
|
||
if err != nil || !versionResp.IsStatusSuccess() {
|
||
service.Success(w, []types.NV{})
|
||
return
|
||
}
|
||
version := strings.TrimSpace(string(versionResp.Bytes()))
|
||
|
||
// 获取运行时间
|
||
uptimeResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+uptime()&user=default&password=%s", port, password))
|
||
uptime := strings.TrimSpace(string(uptimeResp.Bytes()))
|
||
|
||
// 获取当前查询数
|
||
queriesResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+value+FROM+system.metrics+WHERE+metric='Query'&user=default&password=%s", port, password))
|
||
queries := strings.TrimSpace(string(queriesResp.Bytes()))
|
||
|
||
// 获取内存使用
|
||
memResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+value+FROM+system.metrics+WHERE+metric='MemoryTracking'&user=default&password=%s", port, password))
|
||
memUsage := strings.TrimSpace(string(memResp.Bytes()))
|
||
|
||
// 获取数据库数量
|
||
dbCountResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+count()+FROM+system.databases&user=default&password=%s", port, password))
|
||
dbCount := strings.TrimSpace(string(dbCountResp.Bytes()))
|
||
|
||
// 获取表数量
|
||
tableCountResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+count()+FROM+system.tables&user=default&password=%s", port, password))
|
||
tableCount := strings.TrimSpace(string(tableCountResp.Bytes()))
|
||
|
||
data := []types.NV{
|
||
{Name: s.t.Get("Version"), Value: version},
|
||
{Name: s.t.Get("Uptime (seconds)"), Value: uptime},
|
||
{Name: s.t.Get("Active Queries"), Value: queries},
|
||
{Name: s.t.Get("Memory Usage (bytes)"), Value: memUsage},
|
||
{Name: s.t.Get("Databases"), Value: dbCount},
|
||
{Name: s.t.Get("Tables"), Value: tableCount},
|
||
}
|
||
|
||
service.Success(w, data)
|
||
}
|
||
|
||
// GetConfig 获取配置
|
||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||
conf, _ := io.Read(s.configPath())
|
||
service.Success(w, conf)
|
||
}
|
||
|
||
// UpdateConfig 更新配置
|
||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[UpdateConfig](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
|
||
if err = systemctl.Restart("clickhouse-server"); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
|
||
service.Success(w, nil)
|
||
}
|
||
|
||
// GetConfigTune 获取配置调整参数
|
||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||
raw, _ := io.Read(s.configPath())
|
||
var cfg map[string]any
|
||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||
if cfg == nil {
|
||
cfg = make(map[string]any)
|
||
}
|
||
|
||
tune := ConfigTune{
|
||
ListenHost: s.getYAMLValue(cfg, "listen_host"),
|
||
HTTPPort: s.getYAMLValue(cfg, "http_port"),
|
||
TCPPort: s.getYAMLValue(cfg, "tcp_port"),
|
||
MaxMemoryUsage: s.getYAMLValue(cfg, "max_memory_usage"),
|
||
MaxThreads: s.getYAMLValue(cfg, "max_threads"),
|
||
Path: s.getYAMLValue(cfg, "path"),
|
||
TmpPath: s.getYAMLValue(cfg, "tmp_path"),
|
||
LogLevel: s.getYAMLValue(cfg, "logger.level"),
|
||
}
|
||
|
||
service.Success(w, tune)
|
||
}
|
||
|
||
// UpdateConfigTune 更新配置调整参数
|
||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[ConfigTune](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
raw, _ := io.Read(s.configPath())
|
||
var cfg map[string]any
|
||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||
cfg = make(map[string]any)
|
||
}
|
||
|
||
// ClickHouse 顶层键直接用平铺方式
|
||
s.setYAMLValue(cfg, "listen_host", req.ListenHost)
|
||
s.setYAMLValue(cfg, "http_port", req.HTTPPort)
|
||
s.setYAMLValue(cfg, "tcp_port", req.TCPPort)
|
||
s.setYAMLValue(cfg, "max_memory_usage", req.MaxMemoryUsage)
|
||
s.setYAMLValue(cfg, "max_threads", req.MaxThreads)
|
||
s.setYAMLValue(cfg, "path", req.Path)
|
||
s.setYAMLValue(cfg, "tmp_path", req.TmpPath)
|
||
// logger.level 是嵌套的
|
||
s.setNestedYAMLValue(cfg, "logger.level", req.LogLevel)
|
||
|
||
data, err := yaml.Marshal(cfg)
|
||
if err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
if err = io.Write(s.configPath(), string(data), 0644); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
|
||
if err = systemctl.Restart("clickhouse-server"); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
|
||
service.Success(w, nil)
|
||
}
|
||
|
||
// GetDefaultPassword 获取 default 用户密码
|
||
func (s *App) GetDefaultPassword(w http.ResponseWriter, r *http.Request) {
|
||
password, err := s.settingRepo.Get(biz.SettingKeyClickHouseDefaultPassword)
|
||
if err != nil {
|
||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get ClickHouse default password: %v", err))
|
||
return
|
||
}
|
||
|
||
service.Success(w, password)
|
||
}
|
||
|
||
// SetDefaultPassword 设置 default 用户密码
|
||
func (s *App) SetDefaultPassword(w http.ResponseWriter, r *http.Request) {
|
||
req, err := service.Bind[SetDefaultPassword](r)
|
||
if err != nil {
|
||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||
return
|
||
}
|
||
|
||
// 计算 SHA256 哈希
|
||
hash := sha256.Sum256([]byte(req.Password))
|
||
hexHash := fmt.Sprintf("%x", hash)
|
||
|
||
// 读取 users.d/default.yaml 并更新密码
|
||
raw, _ := io.Read(s.usersConfigPath())
|
||
var cfg map[string]any
|
||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||
cfg = make(map[string]any)
|
||
}
|
||
users, _ := cfg["users"].(map[string]any)
|
||
if users == nil {
|
||
users = make(map[string]any)
|
||
cfg["users"] = users
|
||
}
|
||
def, _ := users["default"].(map[string]any)
|
||
if def == nil {
|
||
def = make(map[string]any)
|
||
users["default"] = def
|
||
}
|
||
def["password_sha256_hex"] = hexHash
|
||
out, _ := yaml.Marshal(cfg)
|
||
if err = io.Write(s.usersConfigPath(), string(out), 0644); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to write ClickHouse user config: %v", err))
|
||
return
|
||
}
|
||
|
||
// 重启服务使密码生效
|
||
if err = systemctl.Restart("clickhouse-server"); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||
return
|
||
}
|
||
|
||
// 保存明文到面板数据库
|
||
if err = s.settingRepo.Set(biz.SettingKeyClickHouseDefaultPassword, req.Password); err != nil {
|
||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to save ClickHouse default password: %v", err))
|
||
return
|
||
}
|
||
|
||
_ = s.databaseServerRepo.UpdatePassword("local_clickhouse", req.Password)
|
||
|
||
service.Success(w, nil)
|
||
}
|
||
|
||
// configPath 返回主配置文件路径
|
||
func (s *App) configPath() string {
|
||
return fmt.Sprintf("%s/server/clickhouse/config/config.yaml", app.Root)
|
||
}
|
||
|
||
// usersConfigPath 返回用户密码配置文件路径(users.d/ 由 ConfigProcessor 自动合并到 users.yaml)
|
||
func (s *App) usersConfigPath() string {
|
||
return fmt.Sprintf("%s/server/clickhouse/config/users.d/default.yaml", app.Root)
|
||
}
|
||
|
||
// getPort 从配置中获取 HTTP 端口
|
||
func (s *App) getPort() string {
|
||
raw, _ := io.Read(s.configPath())
|
||
var cfg map[string]any
|
||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||
if cfg != nil {
|
||
if v := s.getYAMLValue(cfg, "http_port"); v != "" {
|
||
return v
|
||
}
|
||
}
|
||
return "8123"
|
||
}
|
||
|
||
// getYAMLValue 获取 YAML 值,支持嵌套键
|
||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||
// 先尝试平铺键
|
||
if val, ok := cfg[key]; ok {
|
||
return cast.ToString(val)
|
||
}
|
||
// 回退到嵌套键
|
||
parts := strings.SplitN(key, ".", 2)
|
||
val, ok := cfg[parts[0]]
|
||
if !ok {
|
||
return ""
|
||
}
|
||
if len(parts) == 1 {
|
||
return cast.ToString(val)
|
||
}
|
||
nested, ok := val.(map[string]any)
|
||
if !ok {
|
||
return ""
|
||
}
|
||
return s.getYAMLValue(nested, parts[1])
|
||
}
|
||
|
||
// setYAMLValue 设置平铺 YAML 值
|
||
func (s *App) setYAMLValue(cfg map[string]any, key string, value string) {
|
||
if value == "" {
|
||
return
|
||
}
|
||
cfg[key] = value
|
||
}
|
||
|
||
// setNestedYAMLValue 设置嵌套 YAML 值
|
||
func (s *App) setNestedYAMLValue(cfg map[string]any, key string, value string) {
|
||
if value == "" {
|
||
return
|
||
}
|
||
parts := strings.SplitN(key, ".", 2)
|
||
if len(parts) == 1 {
|
||
cfg[parts[0]] = value
|
||
return
|
||
}
|
||
nested, ok := cfg[parts[0]].(map[string]any)
|
||
if !ok {
|
||
nested = make(map[string]any)
|
||
cfg[parts[0]] = nested
|
||
}
|
||
s.setNestedYAMLValue(nested, parts[1], value)
|
||
}
|