- 新增 PUT /api/site/logo(multipart、仅图片,立即生效)与 DELETE /api/site/logo,均仅管理员可用 - Logo 文件引用计数自动管理:上传占用、替换/清空/切换外链释放旧文件;PUT /site 手填地址同样处理,挂载不存在的本地文件返回 400 - 抽出 file.IsImageUpload 供头像与 Logo 共用(按文件头探测图片) - 后台管理 Logo 区保留外链输入,新增上传/清空按钮(无裁剪,校验类型与大小),三语文案补齐 - 补充引用计数、权限与校验测试并重新生成 Swagger 文档
238 lines
7.7 KiB
Go
238 lines
7.7 KiB
Go
// Package site 提供站点信息(站名、Logo、页脚文案)的读取与管理员更新接口。
|
|
package site
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
|
|
"rill/internal/auth"
|
|
"rill/internal/config"
|
|
"rill/internal/file"
|
|
"rill/internal/httpx"
|
|
"rill/internal/model"
|
|
)
|
|
|
|
// UpdateRequest 更新站点信息请求,空 logo/footer 表示回退前端默认展示。
|
|
type UpdateRequest struct {
|
|
SiteName string `json:"site_name" binding:"required,max=100" example:"Rill"`
|
|
Logo string `json:"logo" binding:"omitempty,max=500" example:"https://example.com/logo.png"`
|
|
Footer string `json:"footer" binding:"omitempty,max=1000" example:"Copyright © Rill"`
|
|
}
|
|
|
|
// @Summary Get site settings
|
|
// @Description Public site settings: site name, logo URL, and footer text. Returns built-in defaults when the settings row is missing.
|
|
// @Tags public
|
|
// @Produce json
|
|
// @Success 200 {object} model.SiteSetting
|
|
// @Failure 500 {object} httpx.ErrorResponse
|
|
// @Router /site [get]
|
|
func Get(db *gorm.DB) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
var setting model.SiteSetting
|
|
if err := db.WithContext(ctx).First(&setting, model.SiteSettingID).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
c.JSON(http.StatusOK, defaultSetting())
|
|
return
|
|
}
|
|
httpx.RespondDBError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, setting)
|
|
}
|
|
}
|
|
|
|
// @Summary Update site settings
|
|
// @Description Admin only. Update the site name, logo URL, and footer text; logo files hosted on this site have their reference count managed automatically. Returns the updated settings.
|
|
// @Tags admin
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param site body site.UpdateRequest true "Site settings"
|
|
// @Success 200 {object} model.SiteSetting
|
|
// @Failure 400 {object} httpx.ErrorResponse "invalid request or logo file not found"
|
|
// @Security BearerAuth
|
|
// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired"
|
|
// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled"
|
|
// @Failure 500 {object} httpx.ErrorResponse
|
|
// @Router /site [put]
|
|
func Update(db *gorm.DB, cfg *config.Config) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var req UpdateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid request: " + err.Error()})
|
|
return
|
|
}
|
|
req.SiteName = strings.TrimSpace(req.SiteName)
|
|
if req.SiteName == "" {
|
|
c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid request: site_name is required"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
var setting model.SiteSetting
|
|
if err := db.WithContext(ctx).First(&setting, model.SiteSettingID).Error; err != nil {
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
httpx.RespondDBError(c, err)
|
|
return
|
|
}
|
|
setting = defaultSetting()
|
|
}
|
|
|
|
oldID, hasOld := file.ParseLocalURL(cfg.API.Prefix, setting.Logo)
|
|
newID, hasNew := file.ParseLocalURL(cfg.API.Prefix, req.Logo)
|
|
|
|
setting.SiteName = req.SiteName
|
|
setting.Logo = req.Logo
|
|
setting.Footer = req.Footer
|
|
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if hasNew && (!hasOld || newID != oldID) {
|
|
if err := file.Acquire(ctx, tx, newID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if hasOld && (!hasNew || oldID != newID) {
|
|
if err := file.Release(ctx, tx, oldID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Save(&setting).Error
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, file.ErrFileNotFound) {
|
|
c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "invalid request: logo file not found"})
|
|
return
|
|
}
|
|
httpx.RespondDBError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, setting)
|
|
}
|
|
}
|
|
|
|
// @Summary Upload site logo
|
|
// @Description Admin only. Upload an image as the site logo (multipart field file, image only); the logo takes effect immediately and file references are managed automatically.
|
|
// @Tags admin
|
|
// @Accept mpfd
|
|
// @Produce json
|
|
// @Param file formData file true "Logo image"
|
|
// @Success 200 {object} model.SiteSetting
|
|
// @Failure 400 {object} httpx.ErrorResponse "invalid request, empty file, or not an image"
|
|
// @Failure 413 {object} httpx.ErrorResponse "file too large"
|
|
// @Security BearerAuth
|
|
// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired"
|
|
// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled"
|
|
// @Failure 500 {object} httpx.ErrorResponse
|
|
// @Router /site/logo [put]
|
|
func UploadLogo(db *gorm.DB, cfg *config.Config) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
current, ok := auth.CurrentUser(c)
|
|
if !ok {
|
|
httpx.RespondUnauthorized(c)
|
|
return
|
|
}
|
|
header, ok := file.ReadUpload(c, cfg)
|
|
if !ok {
|
|
return
|
|
}
|
|
if isImage, err := file.IsImageUpload(header); err != nil {
|
|
httpx.RespondServerError(c, err, "读取上传图片失败")
|
|
return
|
|
} else if !isImage {
|
|
c.JSON(http.StatusBadRequest, httpx.ErrorResponse{Error: "logo must be an image"})
|
|
return
|
|
}
|
|
src, err := header.Open()
|
|
if err != nil {
|
|
httpx.RespondServerError(c, err, "打开上传图片失败")
|
|
return
|
|
}
|
|
defer src.Close()
|
|
|
|
ctx := c.Request.Context()
|
|
saved, err := file.Save(ctx, db, cfg, file.OperatorOf(c, current), header.Filename, src)
|
|
if err != nil {
|
|
file.RespondSaveError(c, err)
|
|
return
|
|
}
|
|
|
|
var setting model.SiteSetting
|
|
if err := db.WithContext(ctx).First(&setting, model.SiteSettingID).Error; err != nil {
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
httpx.RespondDBError(c, err)
|
|
return
|
|
}
|
|
setting = defaultSetting()
|
|
}
|
|
|
|
oldID, hasOld := file.ParseLocalURL(cfg.API.Prefix, setting.Logo)
|
|
setting.Logo = file.URL(cfg.API.Prefix, saved.ID)
|
|
err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if !hasOld || oldID != saved.ID {
|
|
if err := file.Acquire(ctx, tx, saved.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if hasOld && oldID != saved.ID {
|
|
if err := file.Release(ctx, tx, oldID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Save(&setting).Error
|
|
})
|
|
if err != nil {
|
|
httpx.RespondDBError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, setting)
|
|
}
|
|
}
|
|
|
|
// @Summary Delete site logo
|
|
// @Description Admin only. Clear the site logo and release the reference of the locally stored logo file.
|
|
// @Tags admin
|
|
// @Produce json
|
|
// @Success 200 {object} model.SiteSetting
|
|
// @Security BearerAuth
|
|
// @Failure 401 {object} httpx.ErrorResponse "unauthorized or session expired"
|
|
// @Failure 403 {object} httpx.ErrorResponse "admin permission required or account disabled"
|
|
// @Failure 500 {object} httpx.ErrorResponse
|
|
// @Router /site/logo [delete]
|
|
func DeleteLogo(db *gorm.DB, cfg *config.Config) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
var setting model.SiteSetting
|
|
if err := db.WithContext(ctx).First(&setting, model.SiteSettingID).Error; err != nil {
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
httpx.RespondDBError(c, err)
|
|
return
|
|
}
|
|
setting = defaultSetting()
|
|
}
|
|
|
|
oldID, hasOld := file.ParseLocalURL(cfg.API.Prefix, setting.Logo)
|
|
setting.Logo = ""
|
|
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
if hasOld {
|
|
if err := file.Release(ctx, tx, oldID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Save(&setting).Error
|
|
})
|
|
if err != nil {
|
|
httpx.RespondDBError(c, err)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, setting)
|
|
}
|
|
}
|
|
|
|
// defaultSetting 设置行缺失时的内置默认值。
|
|
func defaultSetting() model.SiteSetting {
|
|
return model.SiteSetting{ID: model.SiteSettingID, SiteName: "Rill"}
|
|
}
|