优化页面、修复BUG

This commit is contained in:
李琦
2026-07-14 09:37:13 +08:00
parent 9a666b9103
commit 8ac9f591a0
18 changed files with 1699 additions and 53 deletions

View File

@@ -0,0 +1,225 @@
package handlers
import (
"errors"
"fmt"
"strings"
"github.com/gin-gonic/gin"
"github.com/niangaodev/art-code/models"
"github.com/niangaodev/art-code/repositories"
"github.com/niangaodev/art-code/utils"
)
func pptTemplatePayloads(list []models.PptTemplate) ([]models.PptTemplatePayload, error) {
out := make([]models.PptTemplatePayload, 0, len(list))
for i := range list {
p, err := repositories.ToPptTemplatePayload(&list[i])
if err != nil {
return nil, err
}
out = append(out, p)
}
return out, nil
}
// GetPptTemplates 公开:获取启用的 PPT 模板
func GetPptTemplates(c *gin.Context) {
list, err := repositories.GetActivePptTemplates()
if err != nil {
utils.ServerError(c, err)
return
}
payloads, err := pptTemplatePayloads(list)
if err != nil {
utils.ServerError(c, err)
return
}
if payloads == nil {
payloads = []models.PptTemplatePayload{}
}
utils.Success(c, payloads)
}
// GetDefaultPptTemplate 公开:获取系统默认模板
func GetDefaultPptTemplate(c *gin.Context) {
t, err := repositories.GetSystemPptTemplate()
if err != nil {
utils.Error(c, 404, "Default template not found")
return
}
payload, err := repositories.ToPptTemplatePayload(t)
if err != nil {
utils.ServerError(c, err)
return
}
utils.Success(c, payload)
}
// AdminGetPptTemplates 管理:获取全部模板
func AdminGetPptTemplates(c *gin.Context) {
list, err := repositories.GetAllPptTemplates()
if err != nil {
utils.ServerError(c, err)
return
}
payloads, err := pptTemplatePayloads(list)
if err != nil {
utils.ServerError(c, err)
return
}
if payloads == nil {
payloads = []models.PptTemplatePayload{}
}
utils.Success(c, payloads)
}
// AdminCreatePptTemplate 管理:创建模板
func AdminCreatePptTemplate(c *gin.Context) {
var req models.PptTemplatePayload
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
req.Name = strings.TrimSpace(req.Name)
req.Slug = strings.TrimSpace(req.Slug)
if req.Name == "" || req.Slug == "" {
utils.Error(c, 400, "Name and slug are required")
return
}
if req.Slug == repositories.DefaultPptTemplateSlug {
utils.Error(c, 400, "Slug 'default' is reserved for system template")
return
}
configJSON, err := repositories.SerializePptTemplateConfig(req.Config)
if err != nil {
utils.Error(c, 400, "Invalid config JSON")
return
}
t := models.PptTemplate{
Name: req.Name,
Slug: req.Slug,
Description: strings.TrimSpace(req.Description),
Config: configJSON,
IsSystem: false,
IsDefault: req.IsDefault,
SortOrder: req.SortOrder,
IsActive: req.IsActive,
}
if err := repositories.CreatePptTemplate(&t); err != nil {
utils.ServerError(c, err)
return
}
if t.IsDefault {
_ = repositories.ClearDefaultPptTemplatesExcept(t.ID)
}
payload, err := repositories.ToPptTemplatePayload(&t)
if err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "PPT template created", payload)
}
// AdminUpdatePptTemplate 管理:更新模板
func AdminUpdatePptTemplate(c *gin.Context) {
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
utils.Error(c, 400, "Invalid ID")
return
}
existing, err := repositories.GetPptTemplateByID(id)
if err != nil {
utils.Error(c, 404, "Template not found")
return
}
var req models.PptTemplatePayload
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "Invalid request")
return
}
req.Name = strings.TrimSpace(req.Name)
if req.Name == "" {
utils.Error(c, 400, "Name is required")
return
}
configJSON, err := repositories.SerializePptTemplateConfig(req.Config)
if err != nil {
utils.Error(c, 400, "Invalid config JSON")
return
}
t := *existing
t.Name = req.Name
t.Description = strings.TrimSpace(req.Description)
t.Config = configJSON
t.SortOrder = req.SortOrder
if existing.IsSystem {
t.Slug = repositories.DefaultPptTemplateSlug
t.IsSystem = true
t.IsActive = true
t.IsDefault = req.IsDefault
} else {
req.Slug = strings.TrimSpace(req.Slug)
if req.Slug == "" {
utils.Error(c, 400, "Slug is required")
return
}
if req.Slug == repositories.DefaultPptTemplateSlug {
utils.Error(c, 400, "Slug 'default' is reserved")
return
}
t.Slug = req.Slug
t.IsDefault = req.IsDefault
t.IsActive = req.IsActive
}
if err := repositories.UpdatePptTemplate(&t); err != nil {
utils.ServerError(c, err)
return
}
if t.IsDefault {
_ = repositories.ClearDefaultPptTemplatesExcept(t.ID)
}
payload, err := repositories.ToPptTemplatePayload(&t)
if err != nil {
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "PPT template updated", payload)
}
// AdminDeletePptTemplate 管理:删除模板
func AdminDeletePptTemplate(c *gin.Context) {
idStr := c.Param("id")
var id uint
if _, err := fmt.Sscanf(idStr, "%d", &id); err != nil {
utils.Error(c, 400, "Invalid ID")
return
}
if err := repositories.DeletePptTemplate(id); err != nil {
if errors.Is(err, repositories.ErrPptTemplateSystemDelete) {
utils.Error(c, 400, "System default template cannot be deleted")
return
}
utils.ServerError(c, err)
return
}
utils.SuccessWithMsg(c, "PPT template deleted", nil)
}

View File

@@ -28,6 +28,7 @@ func main() {
repositories.MigrateUserAvatar()
repositories.MigrateUserProfileFields()
repositories.MigrateVideoModule()
repositories.MigratePptTemplates()
// 初始化ip2region (如果文件不存在,将降级为普通IP记录)
// 函数会自动从环境变量或可执行文件目录查找 ip2region.xdb
@@ -108,6 +109,10 @@ func main() {
api.GET("/testimonials", handlers.GetTestimonials)
api.GET("/partners", handlers.GetPartners)
// PPT 模板
api.GET("/ppt-templates", handlers.GetPptTemplates)
api.GET("/ppt-templates/default", handlers.GetDefaultPptTemplate)
// 咨询相关路由
api.POST("/inquiries", handlers.SubmitInquiry)
api.GET("/email-suffixes", handlers.GetEmailSuffixes)
@@ -258,6 +263,12 @@ func main() {
authAdmin.PUT("/partners/:id", middleware.PermissionMiddleware("settings", "update"), handlers.AdminUpdatePartner)
authAdmin.DELETE("/partners/:id", middleware.PermissionMiddleware("settings", "delete"), handlers.AdminDeletePartner)
// PPT 模板管理 (复用 settings 权限)
authAdmin.GET("/ppt-templates", middleware.PermissionMiddleware("settings", "read"), handlers.AdminGetPptTemplates)
authAdmin.POST("/ppt-templates", middleware.PermissionMiddleware("settings", "create"), handlers.AdminCreatePptTemplate)
authAdmin.PUT("/ppt-templates/:id", middleware.PermissionMiddleware("settings", "update"), handlers.AdminUpdatePptTemplate)
authAdmin.DELETE("/ppt-templates/:id", middleware.PermissionMiddleware("settings", "delete"), handlers.AdminDeletePptTemplate)
// 咨询管理
authAdmin.GET("/inquiries", middleware.PermissionMiddleware("settings", "read"), handlers.AdminGetInquiries)
authAdmin.PUT("/inquiries/:id/status", middleware.PermissionMiddleware("settings", "update"), handlers.AdminUpdateInquiryStatus)

View File

@@ -0,0 +1,58 @@
package models
import (
"time"
"gorm.io/gorm"
)
// PptTemplate PPT 演示模板
type PptTemplate struct {
ID uint `json:"id" gorm:"primaryKey;column:id"`
Name string `json:"name" gorm:"column:name"`
Slug string `json:"slug" gorm:"column:slug;uniqueIndex"`
Description string `json:"description" gorm:"column:description;type:text"`
Config string `json:"-" gorm:"column:config;type:longtext"`
IsSystem bool `json:"isSystem" gorm:"column:is_system;default:0"`
IsDefault bool `json:"isDefault" gorm:"column:is_default;default:0"`
SortOrder int `json:"sortOrder" gorm:"column:sort_order;default:0"`
IsActive bool `json:"isActive" gorm:"column:is_active;default:1"`
CreatedAt int64 `json:"createdAt" gorm:"column:created_at"`
UpdatedAt int64 `json:"updatedAt" gorm:"column:updated_at"`
DeletedAt int64 `json:"deletedAt" gorm:"column:deleted_at;default:0"`
}
func (PptTemplate) TableName() string {
return "ppt_templates"
}
func (t *PptTemplate) BeforeCreate(tx *gorm.DB) error {
now := time.Now().Unix()
if t.CreatedAt == 0 {
t.CreatedAt = now
}
if t.UpdatedAt == 0 {
t.UpdatedAt = now
}
return nil
}
func (t *PptTemplate) BeforeUpdate(tx *gorm.DB) error {
t.UpdatedAt = time.Now().Unix()
return nil
}
// PptTemplatePayload API 请求/响应载荷(config 为 JSON 对象)
type PptTemplatePayload struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Config interface{} `json:"config"`
IsSystem bool `json:"isSystem"`
IsDefault bool `json:"isDefault"`
SortOrder int `json:"sortOrder"`
IsActive bool `json:"isActive"`
CreatedAt int64 `json:"createdAt,omitempty"`
UpdatedAt int64 `json:"updatedAt,omitempty"`
}

View File

@@ -293,3 +293,58 @@ func MigrateVideoModule() {
}
}
}
// MigratePptTemplates 创建 PPT 模板表并补种系统默认模板
func MigratePptTemplates() {
log.Printf("Migrating ppt templates...")
if !tableExists("ppt_templates") {
execSQL(`CREATE TABLE ppt_templates (
id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '模板ID',
name VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板显示名称',
slug VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'URL唯一标识(系统默认为default)',
description TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '模板描述',
config LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'PPT样式配置JSON(配色/动画/切换/UI)',
is_system TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否系统内置模板(0否 1是,不可删除)',
is_default TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否演示默认模板(0否 1是)',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序(越小越靠前)',
is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否启用(0否 1是)',
deleted_at BIGINT NOT NULL DEFAULT 0 COMMENT '软删除时间戳',
created_at BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间(Unix秒)',
updated_at BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间(Unix秒)',
PRIMARY KEY (id),
UNIQUE INDEX idx_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PPT模板表'`)
} else {
ensurePptTemplateColumnComments()
}
EnsureSystemPptTemplate()
}
// ensurePptTemplateColumnComments 为已存在但缺少字段备注的 ppt_templates 表补全 COMMENT
func ensurePptTemplateColumnComments() {
if !tableExists("ppt_templates") {
return
}
alters := []string{
"ALTER TABLE `ppt_templates` MODIFY COLUMN `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '模板ID'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板显示名称'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `slug` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'URL唯一标识(系统默认为default)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '模板描述'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `config` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'PPT样式配置JSON(配色/动画/切换/UI)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `is_system` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否系统内置模板(0否 1是,不可删除)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `is_default` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否演示默认模板(0否 1是)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `sort_order` int NOT NULL DEFAULT 0 COMMENT '排序(越小越靠前)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `is_active` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否启用(0否 1是)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `deleted_at` bigint NOT NULL DEFAULT 0 COMMENT '软删除时间戳'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `created_at` bigint NOT NULL DEFAULT 0 COMMENT '创建时间(Unix秒)'",
"ALTER TABLE `ppt_templates` MODIFY COLUMN `updated_at` bigint NOT NULL DEFAULT 0 COMMENT '更新时间(Unix秒)'",
"ALTER TABLE `ppt_templates` COMMENT='PPT模板表'",
}
for _, sql := range alters {
execSQL(sql)
}
}

View File

@@ -0,0 +1,170 @@
package repositories
import (
"encoding/json"
"errors"
"log"
"time"
"github.com/niangaodev/art-code/config"
"github.com/niangaodev/art-code/models"
)
const DefaultPptTemplateSlug = "default"
const DefaultPptTemplateConfigJSON = `{"colors":{"bg":"#141414","text":"#f5f5f5","muted":"#a0a0a0","accent":"#c9a962","codeBg":"#1e1e1e","codeText":"#eeeeee","border":"rgba(255, 255, 255, 0.1)"},"animation":{"revealDuration":"0.95s","revealLeaveDuration":"0.65s","slideDuration":"0.85s","layoutDuration":"0.75s","revealOffset":"10px","easing":"cubic-bezier(0.22, 1, 0.36, 1)"},"transitions":{"slide":"crossfade","reveal":"fadeUp"},"ui":{"showThumbnailsDefault":true,"canvasBorderRadius":"0.5rem"}}`
var ErrPptTemplateSystemDelete = errors.New("system ppt template cannot be deleted")
func GetActivePptTemplates() ([]models.PptTemplate, error) {
var list []models.PptTemplate
err := config.DB.Model(&models.PptTemplate{}).
Where("deleted_at = ? AND is_active = ?", 0, true).
Order("sort_order ASC, id ASC").
Find(&list).Error
return list, err
}
func GetAllPptTemplates() ([]models.PptTemplate, error) {
var list []models.PptTemplate
err := config.DB.Model(&models.PptTemplate{}).
Where("deleted_at = ?", 0).
Order("sort_order ASC, id ASC").
Find(&list).Error
return list, err
}
func GetPptTemplateByID(id uint) (*models.PptTemplate, error) {
var t models.PptTemplate
err := config.DB.Model(&models.PptTemplate{}).
Where("id = ? AND deleted_at = ?", id, 0).
First(&t).Error
if err != nil {
return nil, err
}
return &t, nil
}
func GetSystemPptTemplate() (*models.PptTemplate, error) {
var t models.PptTemplate
err := config.DB.Model(&models.PptTemplate{}).
Where("slug = ? AND deleted_at = ?", DefaultPptTemplateSlug, 0).
First(&t).Error
if err != nil {
return nil, err
}
return &t, nil
}
func CreatePptTemplate(t *models.PptTemplate) error {
return config.DB.Create(t).Error
}
func UpdatePptTemplate(t *models.PptTemplate) error {
return config.DB.Model(&models.PptTemplate{}).
Where("id = ? AND deleted_at = ?", t.ID, 0).
Updates(map[string]interface{}{
"name": t.Name,
"slug": t.Slug,
"description": t.Description,
"config": t.Config,
"is_default": t.IsDefault,
"sort_order": t.SortOrder,
"is_active": t.IsActive,
"updated_at": time.Now().Unix(),
}).Error
}
func DeletePptTemplate(id uint) error {
t, err := GetPptTemplateByID(id)
if err != nil {
return err
}
if t.IsSystem {
return ErrPptTemplateSystemDelete
}
return config.DB.Model(&models.PptTemplate{}).
Where("id = ?", id).
Update("deleted_at", time.Now().Unix()).Error
}
func ClearDefaultPptTemplatesExcept(id uint) error {
q := config.DB.Model(&models.PptTemplate{}).
Where("deleted_at = ? AND is_default = ?", 0, true)
if id > 0 {
q = q.Where("id <> ?", id)
}
return q.Update("is_default", false).Error
}
func EnsureSystemPptTemplate() {
if !tableExists("ppt_templates") {
return
}
var count int64
config.DB.Model(&models.PptTemplate{}).
Where("slug = ? AND deleted_at = ?", DefaultPptTemplateSlug, 0).
Count(&count)
if count > 0 {
return
}
now := time.Now().Unix()
t := models.PptTemplate{
Name: "默认模板",
Slug: DefaultPptTemplateSlug,
Description: "系统内置 PPT 演示默认样式",
Config: DefaultPptTemplateConfigJSON,
IsSystem: true,
IsDefault: true,
SortOrder: 0,
IsActive: true,
CreatedAt: now,
UpdatedAt: now,
}
if err := config.DB.Create(&t).Error; err != nil {
log.Printf("Failed to seed system ppt template: %v", err)
}
}
func ParsePptTemplateConfigJSON(raw string) (map[string]interface{}, error) {
var cfg map[string]interface{}
if raw == "" {
raw = DefaultPptTemplateConfigJSON
}
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
return nil, err
}
return cfg, nil
}
func SerializePptTemplateConfig(cfg interface{}) (string, error) {
if cfg == nil {
return DefaultPptTemplateConfigJSON, nil
}
b, err := json.Marshal(cfg)
if err != nil {
return "", err
}
return string(b), nil
}
func ToPptTemplatePayload(t *models.PptTemplate) (models.PptTemplatePayload, error) {
cfg, err := ParsePptTemplateConfigJSON(t.Config)
if err != nil {
return models.PptTemplatePayload{}, err
}
return models.PptTemplatePayload{
ID: t.ID,
Name: t.Name,
Slug: t.Slug,
Description: t.Description,
Config: cfg,
IsSystem: t.IsSystem,
IsDefault: t.IsDefault,
SortOrder: t.SortOrder,
IsActive: t.IsActive,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
}, nil
}