feat: Initial commit

This commit is contained in:
engigu
2025-12-20 09:30:16 +08:00
commit 362237241e
189 changed files with 12035 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
package services
import (
"baihu/internal/constant"
"encoding/json"
"os"
)
type ServerConfig struct {
Port int `json:"port"`
Host string `json:"host"`
SiteName string `json:"site_name"`
}
type DatabaseConfig struct {
Type string `json:"type"`
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user"`
Password string `json:"password"`
DBName string `json:"dbname"`
Path string `json:"path"`
TablePrefix string `json:"table_prefix"`
}
type SecurityConfig struct {
JWTSecret string `json:"jwt_secret"`
PasswordSalt string `json:"password_salt"`
}
type TaskConfig struct {
DefaultTimeout int `json:"default_timeout"`
LogRetentionDays int `json:"log_retention_days"`
}
type AppConfig struct {
Server ServerConfig `json:"server"`
Database DatabaseConfig `json:"database"`
Security SecurityConfig `json:"security"`
Task TaskConfig `json:"task"`
}
var Config *AppConfig
func LoadConfig(path string) (*AppConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
Config = &AppConfig{}
if err := json.Unmarshal(data, Config); err != nil {
return nil, err
}
// 设置表前缀到 constant 包
if Config.Database.TablePrefix != "" {
constant.TablePrefix = Config.Database.TablePrefix
}
// 设置 JWT 密钥
if Config.Security.JWTSecret != "" {
constant.JWTSecret = Config.Security.JWTSecret
}
return Config, nil
}
func GetConfig() *AppConfig {
return Config
}
+150
View File
@@ -0,0 +1,150 @@
package services
import (
"sync"
"time"
"baihu/internal/database"
"baihu/internal/logger"
"baihu/internal/models"
"github.com/robfig/cron/v3"
)
// CronService manages scheduled tasks using robfig/cron
type CronService struct {
cron *cron.Cron
taskService *TaskService
executorService *ExecutorService
entryMap map[uint]cron.EntryID // task ID -> cron entry ID
mu sync.RWMutex
}
// NewCronService creates a new cron service
func NewCronService(taskService *TaskService, executorService *ExecutorService) *CronService {
// 使用秒级精度的 cron parser,支持 5 位和 6 位表达式
c := cron.New(cron.WithParser(cron.NewParser(
cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
)))
return &CronService{
cron: c,
taskService: taskService,
executorService: executorService,
entryMap: make(map[uint]cron.EntryID),
}
}
// Start starts the cron service and loads all enabled tasks
func (cs *CronService) Start() {
cs.loadTasks()
cs.cron.Start()
logger.Info("Cron service started")
}
// Stop stops the cron service
func (cs *CronService) Stop() {
ctx := cs.cron.Stop()
<-ctx.Done()
logger.Info("Cron service stopped")
}
// loadTasks loads all enabled tasks from database
func (cs *CronService) loadTasks() {
tasks := cs.taskService.GetTasks()
for _, task := range tasks {
if task.Enabled {
err := cs.AddTask(&task)
if err != nil {
return
}
}
}
}
// AddTask adds a task to the cron scheduler
func (cs *CronService) AddTask(task *models.Task) error {
cs.mu.Lock()
// 如果已存在,先移除
if entryID, exists := cs.entryMap[task.ID]; exists {
cs.cron.Remove(entryID)
delete(cs.entryMap, task.ID)
}
taskID := task.ID
entryID, err := cs.cron.AddFunc(task.Schedule, func() {
cs.runTask(taskID)
})
if err != nil {
cs.mu.Unlock()
logger.Errorf("Failed to add task %d: %v", task.ID, err)
return err
}
cs.entryMap[task.ID] = entryID
cs.mu.Unlock()
logger.Infof("Task %d (%s) scheduled with cron: %s", task.ID, task.Name, task.Schedule)
// 更新下次运行时间
cs.updateNextRun(task.ID)
return nil
}
// RemoveTask removes a task from the cron scheduler
func (cs *CronService) RemoveTask(taskID uint) {
cs.mu.Lock()
defer cs.mu.Unlock()
if entryID, exists := cs.entryMap[taskID]; exists {
cs.cron.Remove(entryID)
delete(cs.entryMap, taskID)
logger.Infof("Task %d removed from scheduler", taskID)
}
}
// runTask executes a task and updates its status
func (cs *CronService) runTask(taskID uint) {
logger.Infof("Running task %d", taskID)
// 更新 last_run
now := time.Now()
database.DB.Model(&models.Task{}).Where("id = ?", taskID).Update("last_run", now)
// 执行任务
cs.executorService.ExecuteTask(int(taskID))
// 更新 next_run
cs.updateNextRun(taskID)
}
// updateNextRun updates the next run time for a task
func (cs *CronService) updateNextRun(taskID uint) {
cs.mu.RLock()
entryID, exists := cs.entryMap[taskID]
cs.mu.RUnlock()
if !exists {
return
}
entry := cs.cron.Entry(entryID)
if !entry.Next.IsZero() {
database.DB.Model(&models.Task{}).Where("id = ?", taskID).Update("next_run", entry.Next)
}
}
// ValidateCron validates a cron expression
func (cs *CronService) ValidateCron(expression string) error {
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
_, err := parser.Parse(expression)
return err
}
// GetScheduledCount returns the number of scheduled tasks
func (cs *CronService) GetScheduledCount() int {
cs.mu.RLock()
defer cs.mu.RUnlock()
return len(cs.entryMap)
}
+68
View File
@@ -0,0 +1,68 @@
package services
import (
"baihu/internal/database"
"baihu/internal/models"
)
type EnvService struct{}
func NewEnvService() *EnvService {
return &EnvService{}
}
func (es *EnvService) CreateEnvVar(name, value, remark string, userID int) *models.EnvironmentVariable {
env := &models.EnvironmentVariable{
Name: name,
Value: value,
Remark: remark,
UserID: uint(userID),
}
database.DB.Create(env)
return env
}
func (es *EnvService) GetEnvVarsByUserID(userID int) []models.EnvironmentVariable {
var envs []models.EnvironmentVariable
database.DB.Where("user_id = ?", userID).Find(&envs)
return envs
}
func (es *EnvService) GetEnvVarsWithPagination(userID int, name string, page, pageSize int) ([]models.EnvironmentVariable, int64) {
var envs []models.EnvironmentVariable
var total int64
query := database.DB.Model(&models.EnvironmentVariable{}).Where("user_id = ?", userID)
if name != "" {
query = query.Where("name LIKE ?", "%"+name+"%")
}
query.Count(&total)
query.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&envs)
return envs, total
}
func (es *EnvService) GetEnvVarByID(id int) *models.EnvironmentVariable {
var env models.EnvironmentVariable
if err := database.DB.First(&env, id).Error; err != nil {
return nil
}
return &env
}
func (es *EnvService) UpdateEnvVar(id int, name, value, remark string) *models.EnvironmentVariable {
var env models.EnvironmentVariable
if err := database.DB.First(&env, id).Error; err != nil {
return nil
}
env.Name = name
env.Value = value
env.Remark = remark
database.DB.Save(&env)
return &env
}
func (es *EnvService) DeleteEnvVar(id int) bool {
result := database.DB.Delete(&models.EnvironmentVariable{}, id)
return result.RowsAffected > 0
}
+183
View File
@@ -0,0 +1,183 @@
package services
import (
"bytes"
"context"
"os/exec"
"sync"
"time"
"baihu/internal/constant"
"baihu/internal/database"
"baihu/internal/logger"
"baihu/internal/models"
"baihu/internal/utils"
)
// ExecutionResult represents the result of a task execution
type ExecutionResult struct {
TaskID int
Success bool
Output string
Error string
Start time.Time
End time.Time
}
// ExecutorService handles task execution
type ExecutorService struct {
taskService *TaskService
results []ExecutionResult
runningTasks map[int]bool // 正在运行的任务
mu sync.RWMutex
}
// NewExecutorService creates a new executor service
func NewExecutorService(taskService *TaskService) *ExecutorService {
return &ExecutorService{
taskService: taskService,
results: make([]ExecutionResult, 0),
runningTasks: make(map[int]bool),
}
}
// ExecuteTask executes a task by ID
func (es *ExecutorService) ExecuteTask(taskID int) *ExecutionResult {
task := es.taskService.GetTaskByID(taskID)
if task == nil {
return &ExecutionResult{
TaskID: taskID,
Success: false,
Error: "Task not found",
Start: time.Now(),
End: time.Now(),
}
}
// 标记任务开始运行
es.mu.Lock()
es.runningTasks[taskID] = true
es.mu.Unlock()
// 使用任务配置的超时时间
timeout := task.Timeout
if timeout <= 0 {
timeout = constant.DefaultTaskTimeout
}
result := es.ExecuteCommandWithTimeout(task.Command, time.Duration(timeout)*time.Minute)
result.TaskID = taskID
// 标记任务结束
es.mu.Lock()
delete(es.runningTasks, taskID)
es.mu.Unlock()
// Save log to database
es.saveTaskLog(uint(taskID), task.Command, result)
return result
}
// GetRunningCount 获取正在运行的任务数量
func (es *ExecutorService) GetRunningCount() int {
es.mu.RLock()
defer es.mu.RUnlock()
return len(es.runningTasks)
}
// ExecuteCommand executes a shell command with default timeout
func (es *ExecutorService) ExecuteCommand(command string) *ExecutionResult {
return es.ExecuteCommandWithTimeout(command, time.Duration(constant.DefaultTaskTimeout)*time.Minute)
}
// ExecuteCommandWithTimeout executes a shell command with specified timeout
func (es *ExecutorService) ExecuteCommandWithTimeout(command string, timeout time.Duration) *ExecutionResult {
result := &ExecutionResult{
Success: false,
Start: time.Now(),
}
// Create a context with timeout
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// Execute the command
shell, args := utils.GetShellCommand(command)
cmd := exec.CommandContext(ctx, shell, args...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
result.End = time.Now()
// Process results
result.Output = stdout.String()
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
result.Error = "执行超时\n" + stderr.String()
} else {
result.Error = err.Error() + "\n" + stderr.String()
}
} else {
result.Success = true
}
// Store result
es.mu.Lock()
es.results = append(es.results, *result)
// Keep only the last 100 results to prevent memory issues
if len(es.results) > 100 {
es.results = es.results[1:]
}
es.mu.Unlock()
return result
}
// GetLastResults returns the last execution results
func (es *ExecutorService) GetLastResults(count int) []ExecutionResult {
es.mu.RLock()
defer es.mu.RUnlock()
start := 0
if len(es.results) > count {
start = len(es.results) - count
}
results := make([]ExecutionResult, len(es.results[start:]))
copy(results, es.results[start:])
return results
}
// saveTaskLog saves execution log to database with gzip+base64 compression
func (es *ExecutorService) saveTaskLog(taskID uint, command string, result *ExecutionResult) {
output := result.Output
if result.Error != "" {
output += "\n[ERROR]\n" + result.Error
}
// Compress output
compressed, err := utils.CompressToBase64(output)
if err != nil {
logger.Errorf("Failed to compress log: %v", err)
compressed = ""
}
status := "success"
if !result.Success {
status = "failed"
}
taskLog := &models.TaskLog{
TaskID: taskID,
Command: command,
Output: compressed,
Status: status,
Duration: result.End.Sub(result.Start).Milliseconds(),
}
if err := database.DB.Create(taskLog).Error; err != nil {
logger.Errorf("Failed to save task log: %v", err)
}
}
+57
View File
@@ -0,0 +1,57 @@
package services
import (
"baihu/internal/logger"
)
const (
InitSection = "system"
InitKey = "initialized"
InitValue = "true"
)
type InitService struct {
settingsService *SettingsService
userService *UserService
}
func NewInitService(settingsService *SettingsService, userService *UserService) *InitService {
return &InitService{
settingsService: settingsService,
userService: userService,
}
}
// Initialize 执行初始化,如果已初始化则跳过
func (s *InitService) Initialize() {
if s.IsInitialized() {
logger.Info("系统已初始化,跳过")
return
}
logger.Info("开始初始化系统...")
// 创建管理员账号
s.createAdminUser()
// 标记为已初始化
s.settingsService.Set(InitSection, InitKey, InitValue)
logger.Info("系统初始化完成")
}
// IsInitialized 检查是否已初始化
func (s *InitService) IsInitialized() bool {
return s.settingsService.Get(InitSection, InitKey) == InitValue
}
// createAdminUser 创建管理员账号
func (s *InitService) createAdminUser() {
existingUser := s.userService.GetUserByUsername("admin")
if existingUser != nil {
logger.Info("管理员账号已存在,跳过创建")
return
}
s.userService.CreateUser("admin", "123456", "admin@local", "admin")
logger.Info("管理员账号创建成功: admin / 123456")
}
+52
View File
@@ -0,0 +1,52 @@
package services
import (
"baihu/internal/database"
"baihu/internal/models"
)
type ScriptService struct{}
func NewScriptService() *ScriptService {
return &ScriptService{}
}
func (ss *ScriptService) CreateScript(name, content string, userID int) *models.Script {
script := &models.Script{
Name: name,
Content: content,
UserID: uint(userID),
}
database.DB.Create(script)
return script
}
func (ss *ScriptService) GetScriptsByUserID(userID int) []models.Script {
var scripts []models.Script
database.DB.Where("user_id = ?", userID).Find(&scripts)
return scripts
}
func (ss *ScriptService) GetScriptByID(id int) *models.Script {
var script models.Script
if err := database.DB.First(&script, id).Error; err != nil {
return nil
}
return &script
}
func (ss *ScriptService) UpdateScript(id int, name, content string) *models.Script {
var script models.Script
if err := database.DB.First(&script, id).Error; err != nil {
return nil
}
script.Name = name
script.Content = content
database.DB.Save(&script)
return &script
}
func (ss *ScriptService) DeleteScript(id int) bool {
result := database.DB.Delete(&models.Script{}, id)
return result.RowsAffected > 0
}
+69
View File
@@ -0,0 +1,69 @@
package services
import (
"baihu/internal/database"
"baihu/internal/models"
)
type SettingsService struct{}
func NewSettingsService() *SettingsService {
return &SettingsService{}
}
// Get 获取设置值
func (s *SettingsService) Get(section, key string) string {
var setting models.Setting
if err := database.DB.Where("section = ? AND key = ?", section, key).First(&setting).Error; err != nil {
return ""
}
return setting.Value
}
// GetWithDefault 获取设置值,如果不存在则返回默认值
func (s *SettingsService) GetWithDefault(section, key, defaultValue string) string {
value := s.Get(section, key)
if value == "" {
return defaultValue
}
return value
}
// Set 设置值
func (s *SettingsService) Set(section, key, value string) error {
var setting models.Setting
result := database.DB.Where("section = ? AND key = ?", section, key).First(&setting)
if result.Error != nil {
// 不存在则创建
setting = models.Setting{
Section: section,
Key: key,
Value: value,
}
return database.DB.Create(&setting).Error
}
// 存在则更新
return database.DB.Model(&setting).Update("value", value).Error
}
// GetBySection 获取某个 section 下的所有设置
func (s *SettingsService) GetBySection(section string) map[string]string {
var settings []models.Setting
database.DB.Where("section = ?", section).Find(&settings)
result := make(map[string]string)
for _, s := range settings {
result[s.Key] = s.Value
}
return result
}
// Delete 删除设置
func (s *SettingsService) Delete(section, key string) error {
return database.DB.Where("section = ? AND key = ?", section, key).Delete(&models.Setting{}).Error
}
// DeleteBySection 删除某个 section 下的所有设置
func (s *SettingsService) DeleteBySection(section string) error {
return database.DB.Where("section = ?", section).Delete(&models.Setting{}).Error
}
+71
View File
@@ -0,0 +1,71 @@
package services
import (
"baihu/internal/database"
"baihu/internal/models"
)
type TaskService struct{}
func NewTaskService() *TaskService {
return &TaskService{}
}
func (ts *TaskService) CreateTask(name, command, schedule string) *models.Task {
task := &models.Task{
Name: name,
Command: command,
Schedule: schedule,
Enabled: true,
}
database.DB.Create(task)
return task
}
func (ts *TaskService) GetTasks() []models.Task {
var tasks []models.Task
database.DB.Find(&tasks)
return tasks
}
// GetTasksWithPagination 分页获取任务列表
func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string) ([]models.Task, int64) {
var tasks []models.Task
var total int64
query := database.DB.Model(&models.Task{})
if name != "" {
query = query.Where("name LIKE ?", "%"+name+"%")
}
query.Count(&total)
query.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&tasks)
return tasks, total
}
func (ts *TaskService) GetTaskByID(id int) *models.Task {
var task models.Task
if err := database.DB.First(&task, id).Error; err != nil {
return nil
}
return &task
}
func (ts *TaskService) UpdateTask(id int, name, command, schedule string, enabled bool) *models.Task {
var task models.Task
if err := database.DB.First(&task, id).Error; err != nil {
return nil
}
task.Name = name
task.Command = command
task.Schedule = schedule
task.Enabled = enabled
database.DB.Save(&task)
return &task
}
func (ts *TaskService) DeleteTask(id int) bool {
result := database.DB.Delete(&models.Task{}, id)
return result.RowsAffected > 0
}
+67
View File
@@ -0,0 +1,67 @@
package services
import (
"crypto/sha256"
"encoding/hex"
"baihu/internal/database"
"baihu/internal/models"
)
type UserService struct{}
func NewUserService() *UserService {
return &UserService{}
}
func (us *UserService) hashPassword(password string) string {
salt := ""
if Config != nil {
salt = Config.Security.PasswordSalt
}
hash := sha256.Sum256([]byte(password + salt))
return hex.EncodeToString(hash[:])
}
func (us *UserService) CreateUser(username, password, email, role string) *models.User {
user := &models.User{
Username: username,
Password: us.hashPassword(password),
Email: email,
Role: role,
}
database.DB.Create(user)
return user
}
func (us *UserService) GetUserByUsername(username string) *models.User {
var user models.User
if err := database.DB.Where("username = ?", username).First(&user).Error; err != nil {
return nil
}
return &user
}
func (us *UserService) ValidatePassword(user *models.User, password string) bool {
return user.Password == us.hashPassword(password)
}
func (us *UserService) EnsureAdminExists() {
var count int64
database.DB.Model(&models.User{}).Where("role = ?", "admin").Count(&count)
if count == 0 {
us.CreateUser("admin", "admin123", "admin@local", "admin")
}
}
func (us *UserService) AuthenticateUser(username, password string) bool {
user := us.GetUserByUsername(username)
if user == nil {
return false
}
return us.ValidatePassword(user, password)
}
func (us *UserService) UpdatePassword(userID uint, newPassword string) error {
return database.DB.Model(&models.User{}).Where("id = ?", userID).Update("password", us.hashPassword(newPassword)).Error
}