From 8d42e84640e0730977784a67c331ba215266b5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Tue, 17 Sep 2024 22:18:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20systemctl=E5=8F=8A=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/biz/monitor.go | 8 ++ internal/biz/setting.go | 8 +- internal/data/monitor.go | 67 +++++++++++++ internal/data/setting.go | 24 +++++ internal/http/request/monitor.go | 11 +++ internal/http/request/setting.go | 16 +++ internal/http/request/systemctl.go | 5 + internal/route/http.go | 2 - internal/service/container_image.go | 8 +- internal/service/container_network.go | 12 +-- internal/service/container_volume.go | 8 +- internal/service/monitor.go | 134 +++++++++++++++++++++++++- internal/service/setting.go | 40 +++++--- internal/service/systemctl.go | 114 ++++++++++++++++++++-- 14 files changed, 418 insertions(+), 39 deletions(-) create mode 100644 internal/data/monitor.go create mode 100644 internal/http/request/monitor.go create mode 100644 internal/http/request/setting.go create mode 100644 internal/http/request/systemctl.go diff --git a/internal/biz/monitor.go b/internal/biz/monitor.go index 1b62d8015f..cc60ac6f7a 100644 --- a/internal/biz/monitor.go +++ b/internal/biz/monitor.go @@ -3,6 +3,7 @@ package biz import ( "github.com/golang-module/carbon/v2" + "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/tools" ) @@ -12,3 +13,10 @@ type Monitor struct { CreatedAt carbon.DateTime `json:"created_at"` UpdatedAt carbon.DateTime `json:"updated_at"` } + +type MonitorRepo interface { + GetSetting() (*request.MonitorSetting, error) + UpdateSetting(setting *request.MonitorSetting) error + Clear() error + List(start, end carbon.Carbon) ([]*Monitor, error) +} diff --git a/internal/biz/setting.go b/internal/biz/setting.go index 144116adab..d09f3c3c5d 100644 --- a/internal/biz/setting.go +++ b/internal/biz/setting.go @@ -1,6 +1,10 @@ package biz -import "github.com/golang-module/carbon/v2" +import ( + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/http/request" +) type SettingKey string @@ -30,4 +34,6 @@ type SettingRepo interface { Get(key SettingKey, defaultValue ...string) (string, error) Set(key SettingKey, value string) error Delete(key SettingKey) error + GetPanelSetting() (*request.PanelSetting, error) + UpdatePanelSetting(setting *request.PanelSetting) error } diff --git a/internal/data/monitor.go b/internal/data/monitor.go new file mode 100644 index 0000000000..48c5304851 --- /dev/null +++ b/internal/data/monitor.go @@ -0,0 +1,67 @@ +package data + +import ( + "errors" + + "github.com/golang-module/carbon/v2" + "github.com/spf13/cast" + + "github.com/TheTNB/panel/internal/app" + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" +) + +type monitorRepo struct { + settingRepo biz.SettingRepo +} + +func NewMonitorRepo() biz.MonitorRepo { + return &monitorRepo{ + settingRepo: NewSettingRepo(), + } +} + +func (r monitorRepo) GetSetting() (*request.MonitorSetting, error) { + monitor, err := r.settingRepo.Get(biz.SettingKeyMonitor) + if err != nil { + return nil, err + } + monitorDays, err := r.settingRepo.Get(biz.SettingKeyMonitorDays) + if err != nil { + return nil, err + } + + setting := new(request.MonitorSetting) + setting.Enabled = cast.ToBool(monitor) + setting.Days = cast.ToInt(monitorDays) + + return setting, nil +} + +func (r monitorRepo) UpdateSetting(setting *request.MonitorSetting) error { + if err := r.settingRepo.Set(biz.SettingKeyMonitor, cast.ToString(setting.Enabled)); err != nil { + return err + } + if err := r.settingRepo.Set(biz.SettingKeyMonitorDays, cast.ToString(setting.Days)); err != nil { + return err + } + + return nil +} + +func (r monitorRepo) Clear() error { + return app.Orm.Delete(&biz.Monitor{}).Error +} + +func (r monitorRepo) List(start, end carbon.Carbon) ([]*biz.Monitor, error) { + var monitors []*biz.Monitor + if err := app.Orm.Where("created_at BETWEEN ? AND ?", start, end).Find(&monitors).Error; err != nil { + return nil, err + } + + if len(monitors) == 0 { + return nil, errors.New("没有找到数据") + } + + return monitors, nil +} diff --git a/internal/data/setting.go b/internal/data/setting.go index ffcdc8fec1..42b0b68e6c 100644 --- a/internal/data/setting.go +++ b/internal/data/setting.go @@ -3,6 +3,7 @@ package data import ( "github.com/TheTNB/panel/internal/app" "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/http/request" ) type settingRepo struct{} @@ -42,3 +43,26 @@ func (r *settingRepo) Delete(key biz.SettingKey) error { return nil } + +func (r *settingRepo) GetPanelSetting() (*request.PanelSetting, error) { + setting := new(biz.Setting) + if err := app.Orm.Where("key = ?", biz.SettingKeyName).First(setting).Error; err != nil { + return nil, err + } + + // TODO fix + + return &request.PanelSetting{ + Name: setting.Value, + }, nil +} + +func (r *settingRepo) UpdatePanelSetting(setting *request.PanelSetting) error { + if err := r.Set(biz.SettingKeyName, setting.Name); err != nil { + return err + } + + // TODO fix + + return nil +} diff --git a/internal/http/request/monitor.go b/internal/http/request/monitor.go new file mode 100644 index 0000000000..82a1543d29 --- /dev/null +++ b/internal/http/request/monitor.go @@ -0,0 +1,11 @@ +package request + +type MonitorSetting struct { + Enabled bool `json:"enabled"` + Days int `json:"days"` +} + +type MonitorList struct { + Start int64 `json:"start"` + End int64 `json:"end"` +} diff --git a/internal/http/request/setting.go b/internal/http/request/setting.go new file mode 100644 index 0000000000..9575e6ba21 --- /dev/null +++ b/internal/http/request/setting.go @@ -0,0 +1,16 @@ +package request + +type PanelSetting struct { + Name string `json:"name"` + Language string `json:"language"` + Entrance string `json:"entrance"` + WebsitePath string `json:"website_path"` + BackupPath string `json:"backup_path"` + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` + Port string `json:"port"` + HTTPS bool `json:"https"` + Cert string `json:"cert"` + Key string `json:"key"` +} diff --git a/internal/http/request/systemctl.go b/internal/http/request/systemctl.go new file mode 100644 index 0000000000..bb3c2ee2eb --- /dev/null +++ b/internal/http/request/systemctl.go @@ -0,0 +1,5 @@ +package request + +type SystemctlService struct { + Service string `json:"service"` +} diff --git a/internal/route/http.go b/internal/route/http.go index c934e38a69..c8fb20fd2a 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -249,8 +249,6 @@ func Http(r chi.Router) { setting := service.NewSettingService() r.Get("/", setting.Get) r.Post("/", setting.Update) - r.Get("/https", setting.GetHttps) - r.Post("/https", setting.UpdateHttps) }) r.Route("/systemctl", func(r chi.Router) { diff --git a/internal/service/container_image.go b/internal/service/container_image.go index d62607e4d5..2cba1dafb2 100644 --- a/internal/service/container_image.go +++ b/internal/service/container_image.go @@ -53,7 +53,7 @@ func (s *ContainerImageService) List(w http.ResponseWriter, r *http.Request) { func (s *ContainerImageService) Exist(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerImageID](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } @@ -69,7 +69,7 @@ func (s *ContainerImageService) Exist(w http.ResponseWriter, r *http.Request) { func (s *ContainerImageService) Pull(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerImagePull](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } @@ -84,7 +84,7 @@ func (s *ContainerImageService) Pull(w http.ResponseWriter, r *http.Request) { func (s *ContainerImageService) Remove(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerImageID](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } @@ -99,7 +99,7 @@ func (s *ContainerImageService) Remove(w http.ResponseWriter, r *http.Request) { func (s *ContainerImageService) Inspect(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerImageID](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } diff --git a/internal/service/container_network.go b/internal/service/container_network.go index 03c486b8ec..c5b7d286e8 100644 --- a/internal/service/container_network.go +++ b/internal/service/container_network.go @@ -70,7 +70,7 @@ func (s *ContainerNetworkService) List(w http.ResponseWriter, r *http.Request) { func (s *ContainerNetworkService) Create(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerNetworkCreate](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } @@ -86,7 +86,7 @@ func (s *ContainerNetworkService) Create(w http.ResponseWriter, r *http.Request) func (s *ContainerNetworkService) Remove(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerNetworkID](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } @@ -101,7 +101,7 @@ func (s *ContainerNetworkService) Remove(w http.ResponseWriter, r *http.Request) func (s *ContainerNetworkService) Exist(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerNetworkID](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } @@ -117,7 +117,7 @@ func (s *ContainerNetworkService) Exist(w http.ResponseWriter, r *http.Request) func (s *ContainerNetworkService) Inspect(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerNetworkID](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } @@ -133,7 +133,7 @@ func (s *ContainerNetworkService) Inspect(w http.ResponseWriter, r *http.Request func (s *ContainerNetworkService) Connect(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerNetworkConnect](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } @@ -148,7 +148,7 @@ func (s *ContainerNetworkService) Connect(w http.ResponseWriter, r *http.Request func (s *ContainerNetworkService) Disconnect(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerNetworkConnect](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } diff --git a/internal/service/container_volume.go b/internal/service/container_volume.go index 6e38ed50cc..6bbe8704a2 100644 --- a/internal/service/container_volume.go +++ b/internal/service/container_volume.go @@ -62,7 +62,7 @@ func (s *ContainerVolumeService) List(w http.ResponseWriter, r *http.Request) { func (s *ContainerVolumeService) Create(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerVolumeCreate](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } @@ -79,7 +79,7 @@ func (s *ContainerVolumeService) Create(w http.ResponseWriter, r *http.Request) func (s *ContainerVolumeService) Exist(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerVolumeID](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } @@ -95,7 +95,7 @@ func (s *ContainerVolumeService) Exist(w http.ResponseWriter, r *http.Request) { func (s *ContainerVolumeService) Remove(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerVolumeID](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } @@ -110,7 +110,7 @@ func (s *ContainerVolumeService) Remove(w http.ResponseWriter, r *http.Request) func (s *ContainerVolumeService) Inspect(w http.ResponseWriter, r *http.Request) { req, err := Bind[request.ContainerVolumeID](r) if err != nil { - Error(w, http.StatusBadRequest, err.Error()) + Error(w, http.StatusUnprocessableEntity, err.Error()) return } diff --git a/internal/service/monitor.go b/internal/service/monitor.go index 4b93a33525..3030e5b4cf 100644 --- a/internal/service/monitor.go +++ b/internal/service/monitor.go @@ -1,26 +1,156 @@ package service -import "net/http" +import ( + "fmt" + "net/http" + + "github.com/golang-module/carbon/v2" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) type MonitorService struct { + settingRepo biz.SettingRepo + monitorRepo biz.MonitorRepo } func NewMonitorService() *MonitorService { - return &MonitorService{} + return &MonitorService{ + settingRepo: data.NewSettingRepo(), + monitorRepo: data.NewMonitorRepo(), + } } func (s *MonitorService) GetSetting(w http.ResponseWriter, r *http.Request) { + setting, err := s.monitorRepo.GetSetting() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + Success(w, setting) } func (s *MonitorService) UpdateSetting(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.MonitorSetting](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + if err = s.monitorRepo.UpdateSetting(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) } func (s *MonitorService) Clear(w http.ResponseWriter, r *http.Request) { + if err := s.monitorRepo.Clear(); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + Success(w, nil) } func (s *MonitorService) List(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.MonitorList](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + monitors, err := s.monitorRepo.List(carbon.CreateFromTimestampMilli(req.Start), carbon.CreateFromTimestampMilli(req.End)) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + type load struct { + Load1 []float64 `json:"load1"` + Load5 []float64 `json:"load5"` + Load15 []float64 `json:"load15"` + } + type cpu struct { + Percent []string `json:"percent"` + } + type mem struct { + Total string `json:"total"` + Available []string `json:"available"` + Used []string `json:"used"` + } + type swap struct { + Total string `json:"total"` + Used []string `json:"used"` + Free []string `json:"free"` + } + type network struct { + Sent []string `json:"sent"` + Recv []string `json:"recv"` + Tx []string `json:"tx"` + Rx []string `json:"rx"` + } + type monitorData struct { + Times []string `json:"times"` + Load load `json:"load"` + Cpu cpu `json:"cpu"` + Mem mem `json:"mem"` + Swap swap `json:"swap"` + Net network `json:"net"` + } + + var data monitorData + var bytesSent uint64 + var bytesRecv uint64 + var bytesSent2 uint64 + var bytesRecv2 uint64 + for _, net := range monitors[0].Info.Net { + if net.Name == "lo" { + continue + } + bytesSent += net.BytesSent + bytesRecv += net.BytesRecv + } + for i, monitor := range monitors { + // 跳过第一条数据,因为第一条数据的流量为 0 + if i == 0 { + // MB + data.Mem.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Total)/1024/1024) + data.Swap.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Total)/1024/1024) + continue + } + for _, net := range monitor.Info.Net { + if net.Name == "lo" { + continue + } + bytesSent2 += net.BytesSent + bytesRecv2 += net.BytesRecv + } + data.Times = append(data.Times, monitor.CreatedAt.ToDateTimeString()) + data.Load.Load1 = append(data.Load.Load1, monitor.Info.Load.Load1) + data.Load.Load5 = append(data.Load.Load5, monitor.Info.Load.Load5) + data.Load.Load15 = append(data.Load.Load15, monitor.Info.Load.Load15) + data.Cpu.Percent = append(data.Cpu.Percent, fmt.Sprintf("%.2f", monitor.Info.Percent[0])) + data.Mem.Available = append(data.Mem.Available, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Available)/1024/1024)) + data.Mem.Used = append(data.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024)) + data.Swap.Used = append(data.Swap.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024)) + data.Swap.Free = append(data.Swap.Free, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Free)/1024/1024)) + data.Net.Sent = append(data.Net.Sent, fmt.Sprintf("%.2f", float64(bytesSent2/1024/1024))) + data.Net.Recv = append(data.Net.Recv, fmt.Sprintf("%.2f", float64(bytesRecv2/1024/1024))) + + // 监控频率为 1 分钟,所以这里除以 60 即可得到每秒的流量 + data.Net.Tx = append(data.Net.Tx, fmt.Sprintf("%.2f", float64(bytesSent2-bytesSent)/60/1024/1024)) + data.Net.Rx = append(data.Net.Rx, fmt.Sprintf("%.2f", float64(bytesRecv2-bytesRecv)/60/1024/1024)) + + bytesSent = bytesSent2 + bytesRecv = bytesRecv2 + bytesSent2 = 0 + bytesRecv2 = 0 + } + Success(w, data) } diff --git a/internal/service/setting.go b/internal/service/setting.go index 932390f769..f30d2ac8c1 100644 --- a/internal/service/setting.go +++ b/internal/service/setting.go @@ -1,26 +1,44 @@ package service -import "net/http" +import ( + "net/http" + + "github.com/TheTNB/panel/internal/biz" + "github.com/TheTNB/panel/internal/data" + "github.com/TheTNB/panel/internal/http/request" +) type SettingService struct { + settingRepo biz.SettingRepo } func NewSettingService() *SettingService { - return &SettingService{} + return &SettingService{ + settingRepo: data.NewSettingRepo(), + } } func (s *SettingService) Get(w http.ResponseWriter, r *http.Request) { + setting, err := s.settingRepo.GetPanelSetting() + if err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + Success(w, setting) } func (s *SettingService) Update(w http.ResponseWriter, r *http.Request) { - -} - -func (s *SettingService) GetHttps(w http.ResponseWriter, r *http.Request) { - -} - -func (s *SettingService) UpdateHttps(w http.ResponseWriter, r *http.Request) { - + req, err := Bind[request.PanelSetting](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = s.settingRepo.UpdatePanelSetting(req); err != nil { + Error(w, http.StatusInternalServerError, err.Error()) + return + } + + Success(w, nil) } diff --git a/internal/service/systemctl.go b/internal/service/systemctl.go index 87fefce74c..0b299efc44 100644 --- a/internal/service/systemctl.go +++ b/internal/service/systemctl.go @@ -1,6 +1,12 @@ package service -import "net/http" +import ( + "fmt" + "net/http" + + "github.com/TheTNB/panel/internal/http/request" + "github.com/TheTNB/panel/pkg/systemctl" +) type SystemctlService struct { } @@ -10,33 +16,123 @@ func NewSystemctlService() *SystemctlService { } func (s *SystemctlService) Status(w http.ResponseWriter, r *http.Request) { - + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + status, err := systemctl.Status(req.Service) + if err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务运行状态失败", req.Service)) + return + } + + Success(w, status) } func (s *SystemctlService) IsEnabled(w http.ResponseWriter, r *http.Request) { - + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + enabled, err := systemctl.IsEnabled(req.Service) + if err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务启用状态失败", req.Service)) + return + } + + Success(w, enabled) } func (s *SystemctlService) Enable(w http.ResponseWriter, r *http.Request) { - + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Enable(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("启用 %s 服务失败", req.Service)) + return + } + + Success(w, nil) } func (s *SystemctlService) Disable(w http.ResponseWriter, r *http.Request) { - + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Disable(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("禁用 %s 服务失败", req.Service)) + return + } + + Success(w, nil) } func (s *SystemctlService) Restart(w http.ResponseWriter, r *http.Request) { - + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Restart(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", req.Service)) + return + } + + Success(w, nil) } func (s *SystemctlService) Reload(w http.ResponseWriter, r *http.Request) { - + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Reload(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("重载 %s 服务失败", req.Service)) + return + } + + Success(w, nil) } func (s *SystemctlService) Start(w http.ResponseWriter, r *http.Request) { - + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Start(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("启动 %s 服务失败", req.Service)) + return + } + + Success(w, nil) } func (s *SystemctlService) Stop(w http.ResponseWriter, r *http.Request) { - + req, err := Bind[request.SystemctlService](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error()) + return + } + + if err = systemctl.Stop(req.Service); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("停止 %s 服务失败", req.Service)) + return + } + + Success(w, nil) }