You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

454 lines
21 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package model
import (
"moredoc/util/captcha"
"strconv"
"time"
jsoniter "github.com/json-iterator/go"
"go.uber.org/zap"
"gorm.io/gorm"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
const (
// ConfigCategorySystem 系统配置系统名称、logo、版权信息、是否闭站等
ConfigCategorySystem = "system"
// ConfigCategoryUser 用户配置:是否启用注册、是否需要审核等
ConfigCategoryUser = "user"
// ConfigCategoryEmail 邮箱配置smtp服务器、端口、用户名、密码、发件人
ConfigCategoryEmail = "email"
// ConfigCategoryCaptcha 验证码配置:是否启用验证码、验证码有效期、验证码长度、验证码类型等
ConfigCategoryCaptcha = "captcha"
// ConfigCategorySecurity 安全配置项
ConfigCategorySecurity = "security"
// ConfigCategoryFooter 底部链接
ConfigCategoryFooter = "footer"
// ConfigCategoryConverter 转换配置项
ConfigCategoryConverter = "converter"
)
type Config struct {
Id int64 `form:"id" json:"id,omitempty" gorm:"primaryKey;autoIncrement;column:id;comment:;"`
Label string `form:"label" json:"label,omitempty" gorm:"column:label;type:varchar(64);size:64;comment:标签名称;"`
Name string `form:"name" json:"name,omitempty" gorm:"column:name;type:varchar(64);size:64;index:name_category,unique;comment:表单字段名称;"`
Value string `form:"value" json:"value,omitempty" gorm:"column:value;type:text;comment:值;"`
Placeholder string `form:"placeholder" json:"placeholder,omitempty" gorm:"column:placeholder;type:varchar(255);size:255;comment:提示信息;"`
InputType string `form:"input_type" json:"input_type,omitempty" gorm:"column:input_type;type:varchar(32);size:32;default:text;comment:输入类型;"`
Category string `form:"category" json:"category,omitempty" gorm:"column:category;type:varchar(32);size:32;index:name_category,unique;index:category;comment:所属类别;"`
Sort int `form:"sort" json:"sort,omitempty" gorm:"column:sort;type:int(11);size:11;default:0;comment:同一category下的排序这里按顺序排序值越小越靠前;"`
Options string `form:"options" json:"options,omitempty" gorm:"column:options;type:text;comment:针对checkbox等的枚举值;"`
CreatedAt *time.Time `form:"created_at" json:"created_at,omitempty" gorm:"column:created_at;type:datetime;comment:创建时间;"`
UpdatedAt *time.Time `form:"updated_at" json:"updated_at,omitempty" gorm:"column:updated_at;type:datetime;comment:更新时间;"`
}
func (Config) TableName() string {
return tablePrefix + "config"
}
// CreateConfig 创建Config
// TODO: 创建成功之后,注意相关表统计字段数值的增减
func (m *DBModel) CreateConfig(config *Config) (err error) {
err = m.db.Create(config).Error
if err != nil {
m.logger.Error("CreateConfig", zap.Error(err))
return
}
return
}
// UpdateConfig 更新Config如果需要更新指定字段则请指定updateFields参数
func (m *DBModel) UpdateConfig(config *Config, updateFields ...string) (err error) {
db := m.db.Model(config)
updateFields = m.FilterValidFields(Config{}.TableName(), updateFields...)
if len(updateFields) > 0 { // 更新指定字段
db = db.Select(updateFields)
}
err = db.Where("id = ?", config.Id).Updates(config).Error
if err != nil {
m.logger.Error("UpdateConfig", zap.Error(err))
}
return
}
// UpdateConfigs 配置项批量更新
// TODO: value值为6个*的,需要特殊处理
func (m *DBModel) UpdateConfigs(configs []*Config, updateFields ...string) (err error) {
sess := m.db.Begin()
defer func() {
if err != nil {
sess.Rollback()
} else {
sess.Commit()
}
}()
tableName := Config{}.TableName()
updateFields = m.FilterValidFields(tableName, updateFields...)
if len(updateFields) == 0 {
updateFields = m.GetTableFields(tableName)
}
for _, config := range configs {
if err = sess.Select(updateFields).Save(config).Error; err != nil {
m.logger.Error("UpdateConfigs", zap.Error(err))
return
}
}
return
}
// GetConfig 根据id获取Config
func (m *DBModel) GetConfig(id interface{}, fields ...string) (config Config, err error) {
db := m.db
fields = m.FilterValidFields(Config{}.TableName(), fields...)
if len(fields) > 0 {
db = db.Select(fields)
}
err = db.Where("id = ?", id).First(&config).Error
return
}
// GetConfigByNameCategory(name string, category string, fields ...string) 根据唯一索引获取Config
func (m *DBModel) GetConfigByNameCategory(name string, category string, fields ...string) (config Config, err error) {
db := m.db
fields = m.FilterValidFields(Config{}.TableName(), fields...)
if len(fields) > 0 {
db = db.Select(fields)
}
db = db.Where("name = ?", name)
db = db.Where("category = ?", category)
err = db.First(&config).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigByNameCategory", zap.Error(err))
return
}
return
}
type OptionGetConfigList struct {
SelectFields []string // 查询字段
QueryIn map[string][]interface{} // map[field][]{value1,value2,...}
}
// GetConfigList 获取Config列表
func (m *DBModel) GetConfigList(opt *OptionGetConfigList) (configList []Config, err error) {
db := m.db.Model(&Config{})
db = m.generateQueryIn(db, Config{}.TableName(), opt.QueryIn)
err = db.Order("sort asc").Find(&configList).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigList", zap.Error(err))
}
return
}
// DeleteConfig 删除数据
// TODO: 删除数据之后,存在 config_id 的关联表,需要删除对应数据,同时相关表的统计数值,也要随着减少
func (m *DBModel) DeleteConfig(ids []interface{}) (err error) {
err = m.db.Where("id in (?)", ids).Delete(&Config{}).Error
if err != nil {
m.logger.Error("DeleteConfig", zap.Error(err))
}
return
}
const (
ConfigCaptchaLength = "length"
ConfigCaptchaWidth = "width"
ConfigCaptchaHeight = "height"
ConfigCaptchaType = "type"
)
type ConfigCaptcha struct {
Length int `json:"length"` // 验证码长度
Width int `json:"width"`
Height int `json:"height"`
Type string `json:"type"` // 验证码类型
}
const (
ConfigSystemSitename = "sitename"
ConfigSystemDomain = "domain"
ConfigSystemTitle = "title"
ConfigSystemDescription = "description"
ConfigSystemKeywords = "keywords"
ConfigSystemLogo = "logo"
ConfigSystemFavicon = "favicon"
ConfigSystemIcp = "icp"
ConfigSystemAnalytics = "analytics"
ConfigSystemCopyrightStartYear = "copyright_start_year"
)
type ConfigSystem struct {
Sitename string `json:"sitename"` // 网站名称
Domain string `json:"domain"` // 站点域名,不带 HTTPS:// 和 HTTP://
Title string `json:"title"` // 网站首页标题
Keywords string `json:"keywords"` // 系统关键字
Description string `json:"description"` // 系统描述
Logo string `json:"logo"` // logo
Favicon string `json:"favicon"` // logo
ICP string `json:"icp"` // 网站备案
Analytics string `json:"analytics"` // 统计代码
CopyrightStartYear string `json:"copyright_start_year"` // 版权年
}
const (
ConfigSecurityMaxDocumentSize = "max_document_size" // 是否关闭注册
ConfigSecurityIsClose = "is_close" // 是否关闭注册
ConfigSecurityCloseStatement = "close_statement" // 闭站说明
ConfigSecurityEnableRegister = "enable_register" // 是否允许注册
ConfigSecurityEnableCaptchaLogin = "enable_captcha_login" // 是否开启登录验证码
ConfigSecurityEnableCaptchaRegister = "enable_captcha_register" // 是否开启注册验证码
ConfigSecurityEnableCaptchaComment = "enable_captcha_comment" // 是否开启注册验证码
ConfigSecurityEnableCaptchaFindPassword = "enable_captcha_find_password" // 是否开启注册验证码
)
type ConfigSecurity struct {
MaxDocumentSize int32 `json:"max_document_size"` // 允许上传的最大文档大小
IsClose bool `json:"is_close"` // 是否闭站
CloseStatement string `json:"close_statement"` // 闭站说明
EnableRegister bool `json:"enable_register"` // 是否启用注册
EnableCaptchaLogin bool `json:"enable_captcha_login"` // 是否启用登录验证码
EnableCaptchaRegister bool `json:"enable_captcha_register"` // 是否启用注册验证码
EnableCaptchaComment bool `json:"enable_captcha_comment"` // 是否启用评论验证码
EnableCaptchaFindPassword bool `json:"enable_captcha_find_password"` // 找回密码是否需要验证码
}
const (
ConfigConverterMaxPreview = "max_preview" // 最大预览页数
ConfigConverterTimeout = "timeout" // 转换超时时间
ConfigConverterEnableSVGO = "enable_svgo" // 是否启用 SVGO
ConfigConverterEnableGZIP = "enable_gzip" // 是否启用 GZIP
)
// ConfigConverter 转换配置
type ConfigConverter struct {
MaxPreview int `json:"max_preview"` // 文档所允许的最大预览页数0 表示不限制,全部转换
Timeout int `json:"timeout"` // 转换超时时间单位为分钟默认30分钟
EnableSVGO bool `json:"enable_svgo"` // 是否对svg启用SVGO压缩。转换效率会有所下降。相对直接的svg文件可以节省1/2的存储空间
EnableGZIP bool `json:"enable_gzip"` // 是否对svg启用GZIP压缩。转换效率会有所下降。相对直接的svg文件可以节省3/4的存储空间
// GZIP和svgo都开启转换效率会有所下降可以综合节省约85%的存储空间
}
const (
ConfigFooterAbout = "about" // 关于我们
ConfigFooterContact = "contact" // 联系我们
ConfigFooterAgreement = "agreement" // 用户协议
ConfigFooterCopyright = "copyright" // 版权信息
ConfigFooterFeedback = "feedback" // 反馈信息
)
type ConfigFooter struct {
About string `json:"about"` // 关于我们
Contact string `json:"contact"` // 联系我们
Agreement string `json:"agreement"` // 用户协议、文库协议
Copyright string `json:"copyright"` // 版权信息、免责声明
Feedback string `json:"feedback"` // 反馈
}
func (m *DBModel) GetConfigOfFooter() (config ConfigFooter) {
var configs []Config
err := m.db.Where("category = ?", ConfigCategoryFooter).Find(&configs).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigOfFooter", zap.Error(err))
}
var data = make(map[string]interface{})
for _, cfg := range configs {
data[cfg.Name] = cfg.Value
}
bytes, _ := json.Marshal(data)
json.Unmarshal(bytes, &config)
return
}
// GetConfigOfCaptcha 获取验证码配置
func (m *DBModel) GetConfigOfCaptcha() (config ConfigCaptcha) {
var configs []Config
err := m.db.Where("category = ?", ConfigCategoryCaptcha).Find(&configs).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigOfCaptcha", zap.Error(err))
}
for _, cfg := range configs {
switch cfg.Name {
case "length":
config.Length, _ = strconv.Atoi(cfg.Value)
if config.Length <= 0 {
config.Length = 6
}
case "width":
config.Width, _ = strconv.Atoi(cfg.Value)
if config.Width <= 0 {
config.Width = 240
}
case "height":
config.Height, _ = strconv.Atoi(cfg.Value)
if config.Height <= 0 {
config.Height = 60
}
case "type":
// 验证码类型
config.Type = cfg.Value
}
}
return
}
// GetConfigOfSystem 获取系统配置
func (m *DBModel) GetConfigOfSystem() (config ConfigSystem) {
var confgis []Config
err := m.db.Where("category = ?", ConfigCategorySystem).Find(&confgis).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigOfSystem", zap.Error(err))
}
var data = make(map[string]interface{})
for _, cfg := range confgis {
data[cfg.Name] = cfg.Value
}
bytes, _ := json.Marshal(data)
json.Unmarshal(bytes, &config)
return
}
// GetConfigOfSecurity 获取安全配置
func (m *DBModel) GetConfigOfSecurity(name ...string) (config ConfigSecurity) {
var configs []Config
db := m.db.Where("category = ?", ConfigCategorySecurity)
if len(name) > 0 {
db = db.Where("name in (?)", name)
}
err := db.Find(&configs).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigOfSecurity", zap.Error(err))
}
var data = make(map[string]interface{})
for _, cfg := range configs {
switch cfg.Name {
case "is_close", "enable_register", "enable_captcha_login", "enable_captcha_register", "enable_captcha_comment", "enable_captcha_find_password", "enable_captcha_upload":
value, _ := strconv.ParseBool(cfg.Value)
data[cfg.Name] = value
case "max_document_size":
data[cfg.Name], _ = strconv.Atoi(cfg.Value)
default:
data[cfg.Name] = cfg.Value
}
}
bytes, _ := json.Marshal(data)
json.Unmarshal(bytes, &config)
return
}
func (m *DBModel) GetConfigOfConverter() (config ConfigConverter) {
var configs []Config
err := m.db.Where("category = ?", ConfigCategoryConverter).Find(&configs).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetConfigOfConverter", zap.Error(err))
}
var data = make(map[string]interface{})
for _, cfg := range configs {
switch cfg.Name {
case "max_preview", "timeout":
data[cfg.Name], _ = strconv.Atoi(cfg.Value)
case "enable_svgo", "enable_gzip":
value, _ := strconv.ParseBool(cfg.Value)
data[cfg.Name] = value
default:
data[cfg.Name] = cfg.Value
}
}
bytes, _ := json.Marshal(data)
json.Unmarshal(bytes, &config)
return
}
func (m *DBModel) initConfig() (err error) {
// 初始化配置项
cfgs := []Config{
// 系统配置项
{Category: ConfigCategorySystem, Name: ConfigSystemSitename, Label: "网站名称", Value: "魔刀文库", Placeholder: "请输入您网站的名称,如:魔刀文库", InputType: "text", Sort: 1, Options: ""},
{Category: ConfigCategorySystem, Name: ConfigSystemTitle, Label: "首页标题", Value: "MOREDOC · 魔刀文库", Placeholder: "请输入您网站的首页标题,如:魔刀文库,强大、专业的文库系统", InputType: "text", Sort: 2, Options: ""},
{Category: ConfigCategorySystem, Name: ConfigSystemKeywords, Label: "网站关键字", Value: "MOREDOC · 魔刀文库", Placeholder: "请输入您网站的关键字", InputType: "text", Sort: 3, Options: ""},
{Category: ConfigCategorySystem, Name: ConfigSystemDescription, Label: "网站描述", Value: "MOREDOC · 魔刀文库", Placeholder: "请输入您网站的描述", InputType: "textarea", Sort: 4, Options: ""},
{Category: ConfigCategorySystem, Name: ConfigSystemLogo, Label: "网站Logo", Value: "", Placeholder: "请输入您网站的Logo路径", InputType: "image", Sort: 6, Options: ""},
{Category: ConfigCategorySystem, Name: ConfigSystemFavicon, Label: "网站Favicon", Value: "", Placeholder: "请输入您网站的Favicon路径", InputType: "image", Sort: 6, Options: ""},
{Category: ConfigCategorySystem, Name: ConfigSystemIcp, Label: "网站备案号", Value: "", Placeholder: "请输入您网站的备案号", InputType: "text", Sort: 6, Options: ""},
{Category: ConfigCategorySystem, Name: ConfigSystemDomain, Label: "网站域名", Value: "https://moredoc.mnt.ltd", Placeholder: "请输入您网站的域名访问地址,如 https://moredoc.mnt.ltd用以生成网站地图sitemap", InputType: "text", Sort: 7, Options: ""},
{Category: ConfigCategorySystem, Name: ConfigSystemCopyrightStartYear, Label: "版权起始年", Value: "2019", Placeholder: "请输入您网站版权起始年2019则前台会显示如 ©2019 - 2022 的字样", InputType: "text", Sort: 8, Options: ""},
{Category: ConfigCategorySystem, Name: ConfigSystemAnalytics, Label: "网站统计代码", Value: "", Placeholder: "请输入您网站的统计代码", InputType: "textarea", Sort: 9, Options: ""},
// 验证码配置项
{Category: ConfigCategoryCaptcha, Name: ConfigCaptchaHeight, Label: "验证码高度", Value: "60", Placeholder: "请输入验证码高度默认为60", InputType: "number", Sort: 13, Options: ""},
{Category: ConfigCategoryCaptcha, Name: ConfigCaptchaWidth, Label: "验证码宽度", Value: "240", Placeholder: "请输入验证码宽度默认为240", InputType: "number", Sort: 14, Options: ""},
{Category: ConfigCategoryCaptcha, Name: ConfigCaptchaLength, Label: "验证码长度", Value: "5", Placeholder: "请输入验证码长度默认为6", InputType: "number", Sort: 15, Options: ""},
{Category: ConfigCategoryCaptcha, Name: ConfigCaptchaType, Label: "验证码类型", Value: "digit", Placeholder: "请选择验证码类型,默认为数字", InputType: "select", Sort: 16, Options: captcha.CaptchaTypeOptions},
// 安全配置项
{Category: ConfigCategorySecurity, Name: ConfigSecurityMaxDocumentSize, Label: "最大文档大小(MB)", Value: "50", Placeholder: "允许用户上传的最大文档大小默认为50即50MB", InputType: "number", Sort: 15, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityIsClose, Label: "是否关闭网站", Value: "false", Placeholder: "请选择是否关闭网站", InputType: "switch", Sort: 16, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityCloseStatement, Label: "闭站说明", Value: "false", Placeholder: "关闭网站后,页面提示的内容", InputType: "textarea", Sort: 17, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableRegister, Label: "是否允许注册", Value: "true", Placeholder: "请选择是否允许用户注册", InputType: "switch", Sort: 18, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaLogin, Label: "是否开启登录验证码", Value: "true", Placeholder: "请选择是否开启登录验证码", InputType: "switch", Sort: 19, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaRegister, Label: "是否开启注册验证码", Value: "true", Placeholder: "请选择是否开启注册验证码", InputType: "switch", Sort: 20, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaComment, Label: "是否开启评论验证码", Value: "true", Placeholder: "请选择是否开启评论验证码", InputType: "switch", Sort: 21, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableCaptchaFindPassword, Label: "是否开启找回密码验证码", Value: "true", Placeholder: "请选择是否开启找回密码验证码", InputType: "switch", Sort: 22, Options: ""},
// 底部链接
{Category: ConfigCategoryFooter, Name: ConfigFooterAbout, Label: "关于我们", Value: "/article/about", Placeholder: "请输入关于我们的链接地址,留空表示不显示", InputType: "text", Sort: 24, Options: ""},
{Category: ConfigCategoryFooter, Name: ConfigFooterContact, Label: "联系我们", Value: "/article/contact", Placeholder: "请输入联系我们的链接地址,留空表示不显示", InputType: "text", Sort: 25, Options: ""},
{Category: ConfigCategoryFooter, Name: ConfigFooterAgreement, Label: "文库协议", Value: "/article/agreement", Placeholder: "请输入文库协议的链接地址,留空表示不显示", InputType: "text", Sort: 26, Options: ""},
{Category: ConfigCategoryFooter, Name: ConfigFooterCopyright, Label: "免责声明", Value: "/article/copyright", Placeholder: "请输入免责声明的链接地址,留空表示不显示", InputType: "text", Sort: 27, Options: ""},
{Category: ConfigCategoryFooter, Name: ConfigFooterFeedback, Label: "意见反馈", Value: "/article/feedback", Placeholder: "请输入意见反馈的链接地址,留空表示不显示", InputType: "text", Sort: 28, Options: ""},
// 转换配置项
{Category: ConfigCategoryConverter, Name: ConfigConverterMaxPreview, Label: "最大预览页数", Value: "0", Placeholder: "文档允许的最大预览页数0表示不限制", InputType: "number", Sort: 15, Options: ""},
{Category: ConfigCategoryConverter, Name: ConfigConverterTimeout, Label: "转换超时(分钟)", Value: "30", Placeholder: "文档转换超时时间默认为30分钟", InputType: "number", Sort: 16, Options: ""},
{Category: ConfigCategoryConverter, Name: ConfigConverterEnableGZIP, Label: "是否启用GZIP压缩", Value: "true", Placeholder: "是否对文档SVG预览文件启用GZIP压缩启用后转换效率会【稍微】下降但相对直接的SVG文件减少75%的存储空间", InputType: "switch", Sort: 17, Options: ""},
{Category: ConfigCategoryConverter, Name: ConfigConverterEnableSVGO, Label: "是否启用SVGO", Value: "false", Placeholder: "是否对文档SVG预览文件启用SVGO压缩启用后转换效率会【明显】下降但相对直接的SVG文件减少50%左右的存储空间", InputType: "switch", Sort: 18, Options: ""},
}
for _, cfg := range cfgs {
existConfig, _ := m.GetConfigByNameCategory(cfg.Name, cfg.Category, "id")
if existConfig.Id > 0 {
// 更新除了值之外的所有字段
cfg.Id = existConfig.Id
err = m.db.Omit("value").Updates(&cfg).Error
if err != nil {
m.logger.Error("initConfig", zap.Error(err))
return
}
continue
}
err = m.CreateConfig(&cfg)
if err != nil {
m.logger.Error("initConfig", zap.Error(err))
return
}
}
return
}