208 lines
8.7 KiB
Go
208 lines
8.7 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"photowall/internal/model"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type ConfigService struct {
|
|
db *gorm.DB
|
|
cache map[string]string
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewConfigService(db *gorm.DB) *ConfigService {
|
|
return &ConfigService{
|
|
db: db,
|
|
cache: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// defaultConfigs 所有可配置项及说明
|
|
var defaultConfigs = []model.SystemConfig{
|
|
// ===== 基础配置 =====
|
|
{Key: "site_name", Value: "PhotoWall 毕业照墙", Label: "站点名称", Description: "显示在浏览器标题和登录页的站点名称", Category: "basic", Placeholder: "PhotoWall 毕业照墙", Sort: 1},
|
|
{Key: "jwt_secret", Value: "photowall-dev-secret-change-in-prod", Label: "JWT 密钥", Description: "用于签发和验证登录Token,生产环境务必修改为随机字符串", Category: "basic", Placeholder: "随机字符串至少32位", Sort: 2},
|
|
{Key: "jwt_expire_hours", Value: "3", Label: "登录有效期(小时)", Description: "用户登录后Token的有效时长,超时需重新登录", Category: "basic", Placeholder: "3", Sort: 3},
|
|
{Key: "audit_grace_days", Value: "30", Label: "审核补齐期限(天)", Description: "班级被打回后,超过该天数未补齐则自动删除", Category: "basic", Placeholder: "30", Sort: 4},
|
|
|
|
// ===== 极验行为验证 =====
|
|
{Key: "geetest_id", Value: "662e68b7ce24be211fa97a53975c5d96", Label: "极验 ID", Description: "极验行为验证的Captcha ID,用于防机器人和暴力破解", Category: "geetest", Placeholder: "如:64a####################", DocLink: "https://www.geetest.com 注册后在应用管理获取", Sort: 1},
|
|
{Key: "geetest_key", Value: "2789075bc2b25a04c0f7c5420bfa3d6f", Label: "极验 Key", Description: "极验行为验证的私钥,与ID配对使用", Category: "geetest", Placeholder: "如:64a####################", DocLink: "https://www.geetest.com 注册后在应用管理获取", Sort: 2},
|
|
|
|
// ===== 短信验证码 =====
|
|
{Key: "sms_provider", Value: "aliyun", Label: "短信服务商", Description: "选择短信服务商,目前支持阿里云和腾讯云", Category: "sms", Placeholder: "aliyun 或 tencent", DocLink: "阿里云: https://dysms.console.aliyun.com | 腾讯云: https://console.cloud.tencent.com/sms", Sort: 1},
|
|
{Key: "sms_access_key", Value: "", Label: "短信 AccessKey", Description: "短信服务商的访问密钥ID", Category: "sms", Placeholder: "如:LTAI5t################", DocLink: "阿里云: https://ram.console.aliyun.com | 腾讯云: https://console.cloud.tencent.com/cam", Sort: 2},
|
|
{Key: "sms_secret_key", Value: "", Label: "短信 SecretKey", Description: "短信服务商的访问密钥密码", Category: "sms", Placeholder: "如:########################", DocLink: "与AccessKey在同一页面获取", Sort: 3},
|
|
{Key: "sms_sign_name", Value: "", Label: "短信签名", Description: "短信签名,需在短信服务商审核通过,如:PhotoWall", Category: "sms", Placeholder: "如:PhotoWall", DocLink: "在短信服务商控制台的国内消息-签名管理中申请", Sort: 4},
|
|
{Key: "sms_template_code", Value: "", Label: "短信模板CODE", Description: "短信模板编码,需在短信服务商审核通过,模板内容需包含验证码变量", Category: "sms", Placeholder: "如:SMS_123456789", DocLink: "在短信服务商控制台的国内消息-模板管理中申请", Sort: 5},
|
|
|
|
// ===== 微信扫码登录 =====
|
|
{Key: "wechat_app_id", Value: "", Label: "微信 AppID", Description: "微信开放平台应用的AppID,用于扫码登录", Category: "wechat", Placeholder: "如:wx################", DocLink: "https://open.weixin.qq.com 注册开发者账号并创建网站应用", Sort: 1},
|
|
{Key: "wechat_app_secret", Value: "", Label: "微信 AppSecret", Description: "微信开放平台应用的密钥,与AppID配对", Category: "wechat", Placeholder: "如:########################", DocLink: "在微信开放平台应用详情中查看", Sort: 2},
|
|
{Key: "wechat_redirect_uri", Value: "", Label: "微信回调地址", Description: "微信授权后的回调URL,需与开放平台配置一致,需HTTPS", Category: "wechat", Placeholder: "如:https://yourdomain.com/api/auth/wechat/callback", DocLink: "在微信开放平台应用的网站应用-授权回调域中配置", Sort: 3},
|
|
|
|
// ===== 存储配置 =====
|
|
{Key: "upload_dir", Value: "uploads", Label: "上传目录", Description: "图片文件存储的相对路径", Category: "storage", Placeholder: "uploads", Sort: 1},
|
|
{Key: "max_file_size_mb", Value: "10", Label: "单文件大小上限(MB)", Description: "上传图片的最大大小限制", Category: "storage", Placeholder: "10", Sort: 2},
|
|
}
|
|
|
|
// InitDefaults 初始化默认配置(不存在则创建,已存在但为空且默认有初始值则补齐)
|
|
func (s *ConfigService) InitDefaults() error {
|
|
for _, c := range defaultConfigs {
|
|
var existing model.SystemConfig
|
|
err := s.db.Where("`key` = ?", c.Key).First(&existing).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
if err := s.db.Create(&c).Error; err != nil {
|
|
return err
|
|
}
|
|
} else if err == nil && existing.Value == "" && c.Value != "" {
|
|
s.db.Model(&existing).Update("value", c.Value)
|
|
}
|
|
}
|
|
|
|
// 预热内存缓存
|
|
var list []model.SystemConfig
|
|
if err := s.db.Find(&list).Error; err == nil {
|
|
s.mu.Lock()
|
|
for _, item := range list {
|
|
s.cache[item.Key] = item.Value
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAll 获取所有配置(按分类和排序)
|
|
func (s *ConfigService) GetAll() ([]model.SystemConfig, error) {
|
|
var list []model.SystemConfig
|
|
err := s.db.Order("category, sort").Find(&list).Error
|
|
return list, err
|
|
}
|
|
|
|
// GetByCategory 按分类获取配置
|
|
func (s *ConfigService) GetByCategory(category string) ([]model.SystemConfig, error) {
|
|
var list []model.SystemConfig
|
|
err := s.db.Where("category = ?", category).Order("sort").Find(&list).Error
|
|
return list, err
|
|
}
|
|
|
|
// UpdateConfigReq 更新配置请求(支持 map 与 key-value 列表格式)
|
|
type UpdateConfigReq struct {
|
|
Configs map[string]string `json:"configs"`
|
|
}
|
|
|
|
func (r *UpdateConfigReq) UnmarshalJSON(data []byte) error {
|
|
// 格式1: {"configs": {"k1": "v1", "k2": "v2"}}
|
|
var auxMap struct {
|
|
Configs map[string]string `json:"configs"`
|
|
}
|
|
if err := json.Unmarshal(data, &auxMap); err == nil && auxMap.Configs != nil {
|
|
r.Configs = auxMap.Configs
|
|
return nil
|
|
}
|
|
|
|
// 格式2: {"configs": [{"key": "k1", "value": "v1"}]}
|
|
var auxList struct {
|
|
Configs []struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
} `json:"configs"`
|
|
}
|
|
if err := json.Unmarshal(data, &auxList); err == nil && auxList.Configs != nil {
|
|
r.Configs = make(map[string]string, len(auxList.Configs))
|
|
for _, item := range auxList.Configs {
|
|
r.Configs[item.Key] = item.Value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 格式3: 直接传入 map: {"k1": "v1"}
|
|
var directMap map[string]string
|
|
if err := json.Unmarshal(data, &directMap); err == nil {
|
|
r.Configs = directMap
|
|
return nil
|
|
}
|
|
|
|
return errors.New("invalid configs format")
|
|
}
|
|
|
|
// Update 批量更新配置
|
|
func (s *ConfigService) Update(req *UpdateConfigReq) error {
|
|
err := s.db.Transaction(func(tx *gorm.DB) error {
|
|
for key, value := range req.Configs {
|
|
if err := tx.Model(&model.SystemConfig{}).Where("`key` = ?", key).Update("value", value).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err == nil {
|
|
s.mu.Lock()
|
|
for key, value := range req.Configs {
|
|
s.cache[key] = value
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// GetValue 获取单个配置值(优先走内存缓存)
|
|
func (s *ConfigService) GetValue(key string) string {
|
|
s.mu.RLock()
|
|
val, ok := s.cache[key]
|
|
s.mu.RUnlock()
|
|
if ok {
|
|
return val
|
|
}
|
|
|
|
var c model.SystemConfig
|
|
if err := s.db.Where("`key` = ?", key).First(&c).Error; err != nil {
|
|
return ""
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.cache[key] = c.Value
|
|
s.mu.Unlock()
|
|
|
|
return c.Value
|
|
}
|
|
|
|
// GetInt 获取整数配置值,解析失败或不存在则返回 def
|
|
func (s *ConfigService) GetInt(key string, def int) int {
|
|
val := s.GetValue(key)
|
|
if val == "" {
|
|
return def
|
|
}
|
|
if n, err := strconv.Atoi(val); err == nil {
|
|
return n
|
|
}
|
|
return def
|
|
}
|
|
|
|
// GetGeetestConfig 获取极验验证配置
|
|
func (s *ConfigService) GetGeetestConfig() (id, key string) {
|
|
return s.GetValue("geetest_id"), s.GetValue("geetest_key")
|
|
}
|
|
|
|
// GetSmsConfig 获取短信配置
|
|
func (s *ConfigService) GetSmsConfig() (provider, accessKey, secretKey, signName, templateCode string) {
|
|
return s.GetValue("sms_provider"),
|
|
s.GetValue("sms_access_key"),
|
|
s.GetValue("sms_secret_key"),
|
|
s.GetValue("sms_sign_name"),
|
|
s.GetValue("sms_template_code")
|
|
}
|
|
|
|
// GetWechatConfig 获取微信登录配置
|
|
func (s *ConfigService) GetWechatConfig() (appID, appSecret, redirectURI string) {
|
|
return s.GetValue("wechat_app_id"),
|
|
s.GetValue("wechat_app_secret"),
|
|
s.GetValue("wechat_redirect_uri")
|
|
}
|