The development mode supports hot reloading of templates

This commit is contained in:
2025-10-26 22:28:50 +08:00
parent a20368a39b
commit edff1bb839
13 changed files with 387 additions and 80 deletions

View File

@@ -105,6 +105,9 @@ func createHTTPServer(addr string) *http.Server {
// 添加日志中间件
router.Use(middleware.WrapHandler())
// 添加开发模式中间件(统一管理开发模式功能)
router.Use(middleware.DevModeMiddleware(router))
// 加载模板
if err := loadTemplates(router); err != nil {
logrus.WithError(err).Fatal("模板加载失败")

View File

@@ -274,6 +274,55 @@ func APIGetTypesHandler(c *gin.Context) {
apiBaseController.HandleSuccess(c, "获取接口类型列表成功", apiTypes)
}
// APIUpdateStatusHandler 更新单个接口状态处理器
func APIUpdateStatusHandler(c *gin.Context) {
var req struct {
ID uint `json:"id"`
Status int `json:"status"`
}
if !apiBaseController.BindJSON(c, &req) {
return
}
if req.ID == 0 {
apiBaseController.HandleValidationError(c, "接口ID不能为空")
return
}
if req.Status != 0 && req.Status != 1 {
apiBaseController.HandleValidationError(c, "状态值无效")
return
}
// 获取数据库连接
db, ok := apiBaseController.GetDB(c)
if !ok {
return
}
// 检查接口是否存在
var api models.API
if err := db.Where("id = ?", req.ID).First(&api).Error; err != nil {
apiBaseController.HandleValidationError(c, "接口不存在")
return
}
// 更新状态
if err := db.Model(&api).Update("status", req.Status).Error; err != nil {
logrus.WithError(err).Error("Failed to update API status")
apiBaseController.HandleInternalError(c, "更新状态失败", err)
return
}
statusText := "禁用"
if req.Status == 1 {
statusText = "启用"
}
apiBaseController.HandleSuccess(c, "接口"+statusText+"成功", nil)
}
func APIGenerateKeysHandler(c *gin.Context) {
var req struct {
Side string `json:"side"` // submit | return

View File

@@ -366,7 +366,7 @@ func AppCreateHandler(c *gin.Context) {
api := models.API{
APIType: apiType,
AppUUID: app.UUID,
Status: 1, // 默认
Status: 0, // 默认
SubmitAlgorithm: models.AlgorithmNone, // 默认不加密
ReturnAlgorithm: models.AlgorithmNone, // 默认不加密
}
@@ -1334,3 +1334,67 @@ func AppsBatchUpdateStatusHandler(c *gin.Context) {
"msg": "批量" + statusText + "成功",
})
}
// AppUpdateStatusHandler 更新单个应用状态处理器
func AppUpdateStatusHandler(c *gin.Context) {
var req struct {
ID uint `json:"id"`
Status int `json:"status"`
}
if !appBaseController.BindJSON(c, &req) {
return
}
if req.ID == 0 {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1,
"msg": "应用ID不能为空",
})
return
}
if req.Status != 0 && req.Status != 1 {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1,
"msg": "状态值无效",
})
return
}
// 获取数据库连接
db, ok := appBaseController.GetDB(c)
if !ok {
return
}
// 检查应用是否存在
var app models.App
if err := db.Where("id = ?", req.ID).First(&app).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1,
"msg": "应用不存在",
})
return
}
// 更新状态
if err := db.Model(&app).Update("status", req.Status).Error; err != nil {
logrus.WithError(err).Error("Failed to update app status")
c.JSON(http.StatusInternalServerError, gin.H{
"code": 1,
"msg": "更新状态失败",
})
return
}
statusText := "禁用"
if req.Status == 1 {
statusText = "启用"
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "应用" + statusText + "成功",
})
}

View File

@@ -9,10 +9,10 @@ import (
"github.com/gin-gonic/gin"
"networkDev/controllers"
"networkDev/middleware"
"networkDev/utils"
"github.com/mojocn/base64Captcha"
"github.com/spf13/viper"
)
// 创建基础控制器实例
@@ -94,7 +94,7 @@ func CaptchaHandler(c *gin.Context) {
// 支持大小写不敏感匹配
func VerifyCaptcha(c *gin.Context, captchaValue string) bool {
// 检查是否为开发模式,如果是则跳过验证码验证
if viper.GetBool("server.dev_mode") {
if middleware.ShouldSkipCaptcha(c) {
return true
}

View File

@@ -4,6 +4,7 @@ import (
"net/http"
"networkDev/constants"
"networkDev/controllers"
"networkDev/middleware"
"networkDev/models"
"networkDev/services"
"networkDev/utils"
@@ -97,7 +98,7 @@ func AdminLayoutHandler(c *gin.Context) {
// - 展示系统信息:版本、开发模式、数据库类型、启动时长
func DashboardFragmentHandler(c *gin.Context) {
version := constants.AppVersion
mode := viper.GetBool("server.dev_mode")
mode := middleware.IsDevModeFromContext(c)
dbType := viper.GetString("database.type")
if dbType == "" {
dbType = "sqlite"
@@ -118,7 +119,7 @@ func DashboardFragmentHandler(c *gin.Context) {
// - 返回系统运行状态的JSON数据用于前端定时刷新
func SystemInfoHandler(c *gin.Context) {
version := constants.AppVersion
mode := viper.GetBool("server.dev_mode")
mode := middleware.IsDevModeFromContext(c)
dbType := viper.GetString("database.type")
if dbType == "" {
dbType = "sqlite"

100
middleware/devmode.go Normal file
View File

@@ -0,0 +1,100 @@
package middleware
import (
"networkDev/web"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
// DevModeConfig 开发模式配置
type DevModeConfig struct {
// 是否启用模板热重载
EnableTemplateReload bool
// 是否跳过验证码验证
SkipCaptcha bool
// 是否显示详细错误信息
ShowDetailedErrors bool
// 是否启用调试日志
EnableDebugLog bool
}
// DevModeMiddleware 开发模式中间件
// 统一管理所有开发模式相关的功能
func DevModeMiddleware(engine *gin.Engine) gin.HandlerFunc {
return func(c *gin.Context) {
// 检查是否为开发模式
if IsDevMode() {
// 设置开发模式标识到上下文
c.Set("dev_mode", true)
c.Set("dev_config", GetDevModeConfig())
// 如果启用了模板热重载,则重新加载模板
config := GetDevModeConfig()
if config.EnableTemplateReload {
reloadTemplates(engine)
}
// 设置开发模式相关的响应头
c.Header("X-Dev-Mode", "true")
} else {
c.Set("dev_mode", false)
}
c.Next()
}
}
// IsDevMode 检查是否为开发模式
func IsDevMode() bool {
return viper.GetBool("server.dev_mode")
}
// GetDevModeConfig 获取开发模式配置
func GetDevModeConfig() DevModeConfig {
if !IsDevMode() {
return DevModeConfig{}
}
return DevModeConfig{
EnableTemplateReload: true, // 开发模式下默认启用模板热重载
SkipCaptcha: true, // 开发模式下默认跳过验证码
ShowDetailedErrors: true, // 开发模式下显示详细错误
EnableDebugLog: true, // 开发模式下启用调试日志
}
}
// IsDevModeFromContext 从上下文中检查是否为开发模式
func IsDevModeFromContext(c *gin.Context) bool {
if devMode, exists := c.Get("dev_mode"); exists {
if isDevMode, ok := devMode.(bool); ok {
return isDevMode
}
}
// 回退到配置检查
return IsDevMode()
}
// GetDevModeConfigFromContext 从上下文中获取开发模式配置
func GetDevModeConfigFromContext(c *gin.Context) DevModeConfig {
if config, exists := c.Get("dev_config"); exists {
if devConfig, ok := config.(DevModeConfig); ok {
return devConfig
}
}
// 回退到默认配置
return GetDevModeConfig()
}
// ShouldSkipCaptcha 检查是否应该跳过验证码验证
func ShouldSkipCaptcha(c *gin.Context) bool {
config := GetDevModeConfigFromContext(c)
return config.SkipCaptcha
}
// reloadTemplates 重新加载模板(内部函数)
func reloadTemplates(engine *gin.Engine) {
if tmpl, err := web.ParseTemplates(); err == nil {
engine.SetHTMLTemplate(tmpl)
}
}

View File

@@ -27,7 +27,7 @@ type API struct {
AppUUID string `gorm:"size:36;not null;index;comment:关联的应用UUID" json:"app_uuid"`
// 接口状态1=启用0=禁用)
Status int `gorm:"default:1;not null;comment:接口状态1=启用0=禁用" json:"status"`
Status int `gorm:"default:0;not null;comment:接口状态1=启用0=禁用" json:"status"`
// 接口提交算法
// 支持的算法0=不加密1=RC42=RSA3=RSA动态4=易加密

View File

@@ -25,7 +25,7 @@ type App struct {
// UUID应用唯一标识符自动生成
UUID string `gorm:"uniqueIndex;size:36;not null;comment:应用UUID唯一标识符" json:"uuid"`
// Status状态1=启用0=禁用json 名称与前端一致
Status int `gorm:"default:1;not null;comment:应用状态1=启用0=禁用" json:"status"`
Status int `gorm:"default:0;not null;comment:应用状态1=启用0=禁用" json:"status"`
// Name应用名称json 名称与前端一致
Name string `gorm:"size:100;not null;comment:应用名称" json:"name"`
// Secret应用密钥用于API认证

View File

@@ -81,6 +81,7 @@ func RegisterAdminRoutes(router *gin.Engine) {
router.POST("/admin/api/apps/delete", adminctl.AdminAuthRequired(), adminctl.AppDeleteHandler)
router.POST("/admin/api/apps/batch_delete", adminctl.AdminAuthRequired(), adminctl.AppsBatchDeleteHandler)
router.POST("/admin/api/apps/batch_update_status", adminctl.AdminAuthRequired(), adminctl.AppsBatchUpdateStatusHandler)
router.POST("/admin/api/apps/update_status", adminctl.AdminAuthRequired(), adminctl.AppUpdateStatusHandler)
router.POST("/admin/api/apps/reset_secret", adminctl.AdminAuthRequired(), adminctl.AppResetSecretHandler)
router.GET("/admin/api/apps/get_app_data", adminctl.AdminAuthRequired(), adminctl.AppGetAppDataHandler)
router.POST("/admin/api/apps/update_app_data", adminctl.AdminAuthRequired(), adminctl.AppUpdateAppDataHandler)
@@ -96,6 +97,7 @@ func RegisterAdminRoutes(router *gin.Engine) {
// API接口管理API
router.GET("/admin/api/apis/list", adminctl.AdminAuthRequired(), adminctl.APIListHandler)
router.POST("/admin/api/apis/update", adminctl.AdminAuthRequired(), adminctl.APIUpdateHandler)
router.POST("/admin/api/apis/update_status", adminctl.AdminAuthRequired(), adminctl.APIUpdateStatusHandler)
router.GET("/admin/api/apis/apps", adminctl.AdminAuthRequired(), adminctl.APIGetAppsHandler)
router.GET("/admin/api/apis/types", adminctl.AdminAuthRequired(), adminctl.APIGetTypesHandler)
router.POST("/admin/api/apis/generate_keys", adminctl.AdminAuthRequired(), adminctl.APIGenerateKeysHandler)

View File

@@ -65,3 +65,9 @@ func GetStaticFS() (fs.FS, error) { // Go 顶级函数不支持箭头写法
}
return staticFS, nil
}
// IsDevMode 检查是否为开发模式
// 注意:这个函数保留用于向后兼容,建议使用 middleware.IsDevMode()
func IsDevMode() bool {
return viper.GetBool("server.dev_mode")
}

View File

@@ -226,10 +226,10 @@
{
field: 'status_name',
title: '状态',
width: 80,
width: 100,
templet: (d) => {
if (d.status === 1) return '<span style="color: #5FB878;">启用</span>';
return '<span style="color: #FF5722;">禁用</span>';
const checked = d.status === 1 ? 'checked' : '';
return `<input type="checkbox" ${checked} lay-skin="switch" lay-text="启用|禁用" lay-filter="api-status-switch" data-id="${d.id}">`;
}
},
{
@@ -626,6 +626,35 @@
}
});
// 接口状态switch开关事件监听
form.on('switch(api-status-switch)', function(data) {
const apiId = data.elem.getAttribute('data-id');
const status = data.elem.checked ? 1 : 0;
$.ajax({
url: '/admin/api/apis/update_status',
type: 'POST',
data: JSON.stringify({ id: parseInt(apiId), status: status }),
contentType: 'application/json',
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg || '状态更新成功', { icon: 1 });
} else {
layer.msg(res.msg || '状态更新失败', { icon: 2 });
// 如果更新失败,恢复开关状态
data.elem.checked = !data.elem.checked;
form.render('checkbox');
}
},
error: function (xhr) {
layer.msg(xhr.responseText || '状态更新失败', { icon: 2 });
// 如果更新失败,恢复开关状态
data.elem.checked = !data.elem.checked;
form.render('checkbox');
}
});
});
});
</script>

View File

@@ -374,8 +374,8 @@
title: '应用状态',
width: 100,
templet: (d) => {
if (d.status === 1) return '<span style="color: #5FB878;">启用</span>';
return '<span style="color: #FF5722;">禁用</span>';
const checked = d.status === 1 ? 'checked' : '';
return `<input type="checkbox" name="app-status-${d.id}" lay-skin="switch" lay-text="启用|禁用" ${checked} lay-filter="app-status-switch" data-id="${d.id}">`;
}
},
{
@@ -1195,6 +1195,35 @@
});
});
// 应用状态switch开关事件监听
form.on('switch(app-status-switch)', function(data) {
const appId = data.elem.getAttribute('data-id');
const status = data.elem.checked ? 1 : 0;
$.ajax({
url: '/admin/api/apps/update_status',
type: 'POST',
data: JSON.stringify({ id: parseInt(appId), status: status }),
contentType: 'application/json',
success: function (res) {
if (res.code === 0) {
layer.msg(res.msg || '状态更新成功', { icon: 1 });
} else {
layer.msg(res.msg || '状态更新失败', { icon: 2 });
// 如果更新失败,恢复开关状态
data.elem.checked = !data.elem.checked;
form.render('checkbox');
}
},
error: function (xhr) {
layer.msg(xhr.responseText || '状态更新失败', { icon: 2 });
// 如果更新失败,恢复开关状态
data.elem.checked = !data.elem.checked;
form.render('checkbox');
}
});
});
// Tips提示功能已移至admin.js统一管理
});
});

View File

@@ -11,25 +11,31 @@
<tbody>
<tr>
<td style="width: 120px; font-weight: bold;">程序版本</td>
<td><span style="font-size: 18px; font-weight: bold; color: var(--lay-color-normal);">{{ .Version }}</span></td>
<td style="height: 20px; vertical-align: middle;">
<span class="layui-badge layui-bg-blue" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">v{{ .Version }}</span>
</td>
</tr>
<tr>
<td style="font-weight: bold;">存储方案</td>
<td><span style="font-size: 18px; font-weight: bold; color: var(--lay-color-info);">{{ .DBType }}</span></td>
<td style="height: 20px; vertical-align: middle;">
<span class="layui-badge layui-bg-cyan" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">{{ .DBType }}</span>
</td>
</tr>
<tr>
<td style="font-weight: bold;">开发模式</td>
<td>
<td style="height: 20px; vertical-align: middle;">
{{ if .Mode }}
<span style="font-size: 18px; font-weight: bold; color: var(--lay-color-danger);">开启</span>
<span class="layui-badge layui-bg-orange" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">开启</span>
{{ else }}
<span style="font-size: 18px; font-weight: bold; color: var(--lay-color-success);">关闭</span>
<span class="layui-badge layui-bg-green" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">关闭</span>
{{ end }}
</td>
</tr>
<tr>
<td style="font-weight: bold;">运行时长</td>
<td><span id="uptime-display" style="font-size: 18px; font-weight: bold; color: var(--lay-color-normal);">{{ .Uptime }}</span></td>
<td style="height: 20px; vertical-align: middle;">
<span id="uptime-display" class="layui-badge layui-bg-gray" style="font-size: 14px; padding: 2px 8px; line-height: 1.2;">{{ .Uptime }}</span>
</td>
</tr>
</tbody>
</table>
@@ -46,19 +52,27 @@
<tbody>
<tr>
<td style="width: 120px; font-weight: bold;">全部应用</td>
<td><span id="total-apps" style="font-size: 18px; font-weight: bold;">0</span></td>
<td style="height: 20px; vertical-align: middle;">
<span id="total-apps" class="layui-badge" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
</td>
</tr>
<tr>
<td style="font-weight: bold;">启用应用</td>
<td><span id="enabled-apps" style="font-size: 18px; font-weight: bold; color: var(--lay-color-success);">0</span></td>
<td style="height: 20px; vertical-align: middle;">
<span id="enabled-apps" class="layui-badge layui-bg-green" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
</td>
</tr>
<tr>
<td style="font-weight: bold;">禁用应用</td>
<td><span id="disabled-apps" style="font-size: 18px; font-weight: bold; color: var(--lay-color-danger);">0</span></td>
<td style="height: 20px; vertical-align: middle;">
<span id="disabled-apps" class="layui-badge layui-bg-orange" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
</td>
</tr>
<tr>
<td style="font-weight: bold;">变量数量</td>
<td><span id="total-variables" style="font-size: 18px; font-weight: bold; color: var(--lay-color-info);">0</span></td>
<td style="height: 20px; vertical-align: middle;">
<span id="total-variables" class="layui-badge layui-bg-blue" style="font-size: 14px; padding: 2px 8px; min-width: 30px; text-align: center; line-height: 1.2;">0</span>
</td>
</tr>
</tbody>
</table>
@@ -98,9 +112,19 @@
$.get('/admin/api/system/info', (res) => {
if (res && res.code === 0 && res.data) {
const data = res.data;
// 更新运行时长
// 更新运行时长,保持徽章样式
if (data.uptime) {
$('#uptime-display').text(data.uptime);
const uptimeElement = $('#uptime-display');
uptimeElement.text(data.uptime);
// 确保徽章样式保持一致
if (!uptimeElement.hasClass('layui-badge')) {
uptimeElement.addClass('layui-badge layui-bg-gray');
uptimeElement.css({
'font-size': '14px',
'padding': '2px 8px',
'line-height': '1.2'
});
}
}
}
}).fail(() => {