diff --git a/douya b/douya index 473591d..2ab69f3 100644 Binary files a/douya and b/douya differ diff --git a/internal/database/database.go b/internal/database/database.go index 49623ad..823ecfd 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -67,6 +67,7 @@ func Init(cfg *config.DatabaseConfig) (*DB, error) { &model.GameCategory{}, &model.Game{}, &model.AppCategory{}, &model.App{}, &model.MusicCategory{}, &model.Song{}, + &model.AppShowcase{}, &model.BizSystem{}, ); err != nil { return nil, fmt.Errorf("自动建表失败: %w", err) } diff --git a/internal/handler/config.go b/internal/handler/config.go index f3dda1f..6118475 100644 --- a/internal/handler/config.go +++ b/internal/handler/config.go @@ -142,6 +142,11 @@ func (h *ConfigHandler) GetNavSettings(c *gin.Context) { "nav_sec_app_sub", "nav_sec_music_title", "nav_sec_music_sub", + // 新板块 + "nav_sec_appshow_title", + "nav_sec_appshow_sub", + "nav_sec_bizsys_title", + "nav_sec_bizsys_sub", } settings := map[string]string{} diff --git a/internal/handler/showcase.go b/internal/handler/showcase.go new file mode 100644 index 0000000..7c76937 --- /dev/null +++ b/internal/handler/showcase.go @@ -0,0 +1,191 @@ +package handler + +import ( + "strconv" + + "gotest/internal/model" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// ShowcaseHandler App 候选码 & 业务系统管理 +type ShowcaseHandler struct { + db *gorm.DB +} + +func NewShowcaseHandler(db *gorm.DB) *ShowcaseHandler { + return &ShowcaseHandler{db: db} +} + +// ========== App 候选码 ========== + +func (h *ShowcaseHandler) ListAppShowcase(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + var total int64 + h.db.Model(&model.AppShowcase{}).Where("status = 1").Count(&total) + + var items []model.AppShowcase + h.db.Where("status = 1").Order("sort ASC, id DESC"). + Offset((page - 1) * pageSize).Limit(pageSize).Find(&items) + + Success(c, gin.H{ + "list": items, "total": total, "page": page, "page_size": pageSize, + }) +} + +func (h *ShowcaseHandler) AdminListAppShowcase(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + var total int64 + h.db.Model(&model.AppShowcase{}).Count(&total) + + var items []model.AppShowcase + h.db.Order("sort ASC, id DESC"). + Offset((page - 1) * pageSize).Limit(pageSize).Find(&items) + + Success(c, gin.H{ + "list": items, "total": total, "page": page, "page_size": pageSize, + }) +} + +func (h *ShowcaseHandler) CreateAppShowcase(c *gin.Context) { + var item model.AppShowcase + if err := c.ShouldBindJSON(&item); err != nil { + FailBadRequest(c, "参数错误") + return + } + if err := h.db.Create(&item).Error; err != nil { + FailInternal(c, "创建失败: "+err.Error()) + return + } + Success(c, item) +} + +func (h *ShowcaseHandler) UpdateAppShowcase(c *gin.Context) { + id := c.Param("id") + var item model.AppShowcase + if err := h.db.First(&item, id).Error; err != nil { + Fail(c, 404, "记录不存在") + return + } + if err := c.ShouldBindJSON(&item); err != nil { + FailBadRequest(c, "参数错误") + return + } + if err := h.db.Save(&item).Error; err != nil { + FailInternal(c, "更新失败: "+err.Error()) + return + } + Success(c, item) +} + +func (h *ShowcaseHandler) DeleteAppShowcase(c *gin.Context) { + id := c.Param("id") + if err := h.db.Delete(&model.AppShowcase{}, id).Error; err != nil { + FailInternal(c, "删除失败: "+err.Error()) + return + } + SuccessMsg(c, "删除成功") +} + +// ========== 业务系统 ========== + +func (h *ShowcaseHandler) ListBizSystem(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + var total int64 + h.db.Model(&model.BizSystem{}).Where("status = 1").Count(&total) + + var items []model.BizSystem + h.db.Where("status = 1").Order("sort ASC, id DESC"). + Offset((page - 1) * pageSize).Limit(pageSize).Find(&items) + + Success(c, gin.H{ + "list": items, "total": total, "page": page, "page_size": pageSize, + }) +} + +func (h *ShowcaseHandler) AdminListBizSystem(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + + var total int64 + h.db.Model(&model.BizSystem{}).Count(&total) + + var items []model.BizSystem + h.db.Order("sort ASC, id DESC"). + Offset((page - 1) * pageSize).Limit(pageSize).Find(&items) + + Success(c, gin.H{ + "list": items, "total": total, "page": page, "page_size": pageSize, + }) +} + +func (h *ShowcaseHandler) CreateBizSystem(c *gin.Context) { + var item model.BizSystem + if err := c.ShouldBindJSON(&item); err != nil { + FailBadRequest(c, "参数错误") + return + } + if err := h.db.Create(&item).Error; err != nil { + FailInternal(c, "创建失败: "+err.Error()) + return + } + Success(c, item) +} + +func (h *ShowcaseHandler) UpdateBizSystem(c *gin.Context) { + id := c.Param("id") + var item model.BizSystem + if err := h.db.First(&item, id).Error; err != nil { + Fail(c, 404, "记录不存在") + return + } + if err := c.ShouldBindJSON(&item); err != nil { + FailBadRequest(c, "参数错误") + return + } + if err := h.db.Save(&item).Error; err != nil { + FailInternal(c, "更新失败: "+err.Error()) + return + } + Success(c, item) +} + +func (h *ShowcaseHandler) DeleteBizSystem(c *gin.Context) { + id := c.Param("id") + if err := h.db.Delete(&model.BizSystem{}, id).Error; err != nil { + FailInternal(c, "删除失败: "+err.Error()) + return + } + SuccessMsg(c, "删除成功") +} diff --git a/internal/model/showcase.go b/internal/model/showcase.go new file mode 100644 index 0000000..2a002b4 --- /dev/null +++ b/internal/model/showcase.go @@ -0,0 +1,49 @@ +package model + +import "time" + +// AppShowcase App 候选码展示模型 +type AppShowcase struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` // App 名称 + Icon string `gorm:"size:255" json:"icon"` // 图标 URL + Description string `gorm:"type:text" json:"description"` // 简介 + Category string `gorm:"size:50" json:"category"` // 分类:工具/社交/教育/生活/娱乐 + Platform string `gorm:"size:100" json:"platform"` // 平台:Android/iOS/跨平台 + Version string `gorm:"size:50" json:"version"` // 版本 + Size string `gorm:"size:50" json:"size"` // 包大小 + Downloads int `gorm:"default:0" json:"downloads"` // 下载量 + Rating float64 `gorm:"type:decimal(2,1);default:5.0" json:"rating"` // 评分 + Screenshots string `gorm:"type:text" json:"screenshots"` // JSON: 截图 URL 数组 + VideoUrl string `gorm:"size:500" json:"video_url"` // 录屏视频 URL + DownloadUrl string `gorm:"size:500" json:"download_url"` // 下载链接 + Tags string `gorm:"type:text" json:"tags"` // JSON: 标签 + IsFeatured bool `gorm:"default:false" json:"is_featured"` // 精选推荐 + Sort int `gorm:"default:0" json:"sort"` + Status int `gorm:"default:1;not null" json:"status"` // 1=上架 0=下架 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// BizSystem 业务系统模型 +type BizSystem struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"size:100;not null" json:"name"` // 系统名称 + Icon string `gorm:"size:255" json:"icon"` // 图标 + Logo string `gorm:"size:255" json:"logo"` // Logo 大图 + Description string `gorm:"type:text" json:"description"` // 一句话介绍 + Detail string `gorm:"type:text" json:"detail"` // 详细说明(富文本) + Category string `gorm:"size:50" json:"category"` // 分类:进销存/OA/财务/工单/ERP/CRM/自定义 + Features string `gorm:"type:text" json:"features"` // JSON: 核心功能列表 + Screenshots string `gorm:"type:text" json:"screenshots"` // JSON: 系统截图 URL 数组 + PriceType string `gorm:"size:50" json:"price_type"` // 价格类型:按年/按月/一次性/定制 + Price string `gorm:"size:100" json:"price"` // 价格描述:如 "3000元/年" + DemoUrl string `gorm:"size:500" json:"demo_url"` // 在线演示链接 + ContactWay string `gorm:"size:50" json:"contact_way"` // 联系方式类型:微信/电话/表单 + ContactInfo string `gorm:"size:200" json:"contact_info"` // 联系方式内容 + IsFeatured bool `gorm:"default:false" json:"is_featured"` // 精选推荐 + Sort int `gorm:"default:0" json:"sort"` + Status int `gorm:"default:1;not null" json:"status"` // 1=上线 0=下线 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/router/router.go b/internal/router/router.go index e263574..f554ac9 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -32,6 +32,7 @@ func Setup( accessLogH *handler.AccessLogHandler, configH *handler.ConfigHandler, ipLocationService *geoip.Service, + showcaseH *handler.ShowcaseHandler, ) *gin.Engine { if cfg.Env.Name == "prod" { gin.SetMode(gin.ReleaseMode) @@ -165,6 +166,10 @@ func Setup( // 导航页设置(公开,无需认证) api.GET("/nav-settings", configH.GetNavSettings) + // App 候选码 & 业务系统(公开) + api.GET("/showcase/apps", showcaseH.ListAppShowcase) + api.GET("/showcase/systems", showcaseH.ListBizSystem) + // 需要登录的路由 authed := api.Group("") authed.Use(middleware.JWTAuth(cfg.JWT.Secret)) @@ -212,6 +217,18 @@ func Setup( // Webhook 测试 admin.POST("/webhook/test", customerH.TestWebhook) + + // App 候选码管理 + admin.GET("/showcase/apps", showcaseH.AdminListAppShowcase) + admin.POST("/showcase/apps", showcaseH.CreateAppShowcase) + admin.PUT("/showcase/apps/:id", showcaseH.UpdateAppShowcase) + admin.DELETE("/showcase/apps/:id", showcaseH.DeleteAppShowcase) + + // 业务系统管理 + admin.GET("/showcase/systems", showcaseH.AdminListBizSystem) + admin.POST("/showcase/systems", showcaseH.CreateBizSystem) + admin.PUT("/showcase/systems/:id", showcaseH.UpdateBizSystem) + admin.DELETE("/showcase/systems/:id", showcaseH.DeleteBizSystem) } } diff --git a/main.go b/main.go index d0c02e5..32382fa 100644 --- a/main.go +++ b/main.go @@ -92,12 +92,13 @@ func main() { customerHandler := handler.NewCustomerHandler(db.DB, cfg) accessLogHandler := handler.NewAccessLogHandler(db.DB) configHandler := handler.NewConfigHandler(db.DB) + showcaseHandler := handler.NewShowcaseHandler(db.DB) // IP 归属地查询服务 ipLocationService := geoip.NewService() // 8. 设置路由 - r := router.Setup(cfg, db, authHandler, userHandler, adminHandler, uploadHandler, enterpriseHandler, shopHandler, foodHandler, gameHandler, appHandler, musicHandler, dashboardHandler, customerHandler, accessLogHandler, configHandler, ipLocationService) + r := router.Setup(cfg, db, authHandler, userHandler, adminHandler, uploadHandler, enterpriseHandler, shopHandler, foodHandler, gameHandler, appHandler, musicHandler, dashboardHandler, customerHandler, accessLogHandler, configHandler, ipLocationService, showcaseHandler) // 9. 配置嵌入式前端:18 套网站模板 setupFrontend(r) diff --git a/web/dist/admin/index.html b/web/dist/admin/index.html index 450f86b..04f80ba 100644 --- a/web/dist/admin/index.html +++ b/web/dist/admin/index.html @@ -276,6 +276,12 @@ tbody tr:last-child td{border-bottom:none} 📝 导航页设置 + + 📱 App候选码 + + + 💼 业务系统 + ⚙️ 系统设置 @@ -458,7 +464,7 @@ function navigate(page, params = {}) { document.querySelectorAll('.sidebar-link').forEach(el => el.classList.remove('active')); const link = document.querySelector(`[data-page="${page}"]`); if (link) link.classList.add('active'); - const titles = { users: '用户管理', content: '内容管理', settings: '系统设置', sites: '站点导航', accesslog: '访问日志', webhook: 'Webhook配置', navsettings: '导航页设置' }; + const titles = { users: '用户管理', content: '内容管理', settings: '系统设置', sites: '站点导航', accesslog: '访问日志', webhook: 'Webhook配置', navsettings: '导航页设置', appshowcase: 'App候选码', bizsystems: '业务系统' }; document.getElementById('pageTitle').textContent = titles[page] || page; if (page === 'users') renderUsers(); else if (page === 'content') renderContent(params.table); @@ -467,6 +473,8 @@ function navigate(page, params = {}) { else if (page === 'accesslog') renderAccessLogs(); else if (page === 'webhook') renderWebhook(); else if (page === 'navsettings') renderNavSettings(); + else if (page === 'appshowcase') renderAppShowcase(); + else if (page === 'bizsystems') renderBizSystems(); if (window.innerWidth <= 768) document.getElementById('sidebar').classList.remove('open'); } @@ -555,7 +563,7 @@ function openUserModal(id) { ` : ''} `; - document.getElementById('modalSaveBtn').onclick = saveUser; + _modalSaveFn = saveUser; showModal(); } @@ -1342,6 +1350,14 @@ async function renderNavSettings() {
+
+
+
+
+
+
+
+
@@ -1366,6 +1382,7 @@ async function saveNavSettings() { 'sec_enterprise_title','sec_enterprise_sub','sec_shop_title','sec_shop_sub', 'sec_food_title','sec_food_sub','sec_game_title','sec_game_sub', 'sec_app_title','sec_app_sub','sec_music_title','sec_music_sub', + 'sec_appshow_title','sec_appshow_sub','sec_bizsys_title','sec_bizsys_sub', 'consult_title','consult_desc','consult_success','footer_text','company_name' ]; const items = fields.map(f => ({ @@ -1382,6 +1399,203 @@ async function saveNavSettings() { } catch (e) { toast('保存失败', 'error'); } } +// ===== App 候选码管理 ===== +let appShowPage = 1; +async function renderAppShowcase(page) { + if (page) appShowPage = page; + const el = document.getElementById('content'); + el.innerHTML = '
'; + try { + const data = await api('/admin/showcase/apps?page=' + appShowPage + '&page_size=20'); + const d = data.data; + el.innerHTML = ` +
+
+

📱 App 候选码管理

+ +
+
+ + + + ${d.list.length===0?'':d.list.map(a=>` + + + + + + + + + + `).join('')} + +
ID名称分类平台版本评分状态操作
暂无数据
${a.id}${a.name}${a.category||'-'}${a.platform||'-'}${a.version||'-'}⭐${a.rating}${a.status===1?'上架':'下架'} + + +
+
+ ${renderPagination(d,'renderAppShowcase')} +
`; + } catch(e) { el.innerHTML = '
加载失败
'; } +} + +let appShowData = []; +async function openAppShowModal(id) { + if (!appShowData.length) { try { const r=await api('/admin/showcase/apps?page_size=100'); appShowData=r.data.list||[]; } catch(e){} } + const item = id ? appShowData.find(a=>a.id===id) : null; + document.getElementById('modalTitle').textContent = id ? '编辑App' : '新增App'; + document.getElementById('modalBody').innerHTML = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`; + editingId = id; + _modalSaveFn = saveAppShow; + openModal(); +} + +async function saveAppShow() { + const body = { + name: document.getElementById('m_name').value, + category: document.getElementById('m_category').value, + platform: document.getElementById('m_platform').value, + version: document.getElementById('m_version').value, + description: document.getElementById('m_description').value, + icon: document.getElementById('m_icon').value, + download_url: document.getElementById('m_download_url').value, + video_url: document.getElementById('m_video_url').value, + size: document.getElementById('m_size').value, + rating: parseFloat(document.getElementById('m_rating').value)||5, + sort: parseInt(document.getElementById('m_sort').value)||0, + status: parseInt(document.getElementById('m_status').value), + }; + try { + if (editingId) { + await api('/admin/showcase/apps/' + editingId, { method:'PUT', body:JSON.stringify(body) }); + } else { + await api('/admin/showcase/apps', { method:'POST', body:JSON.stringify(body) }); + } + closeModal(); toast('保存成功','success'); renderAppShowcase(); + } catch(e) {} +} + +async function deleteAppShow(id) { + if (!confirm('确定删除?')) return; + try { await api('/admin/showcase/apps/'+id,{method:'DELETE'}); toast('删除成功','success'); renderAppShowcase(); } catch(e){} +} + +// ===== 业务系统管理 ===== +let bizSysPage = 1; +async function renderBizSystems(page) { + if (page) bizSysPage = page; + const el = document.getElementById('content'); + el.innerHTML = '
'; + try { + const data = await api('/admin/showcase/systems?page=' + bizSysPage + '&page_size=20'); + const d = data.data; + el.innerHTML = ` +
+
+

💼 业务系统管理

+ +
+
+ + + + ${d.list.length===0?'':d.list.map(s=>` + + + + + + + + + `).join('')} + +
ID名称分类价格类型价格状态操作
暂无数据
${s.id}${s.name}${s.category||'-'}${s.price_type||'-'}${s.price||'-'}${s.status===1?'上线':'下线'} + + +
+
+ ${renderPagination(d,'renderBizSystems')} +
`; + } catch(e) { el.innerHTML = '
加载失败
'; } +} + +let bizSysData = []; +async function openBizSysModal(id) { + if (!bizSysData.length) { try { const r=await api('/admin/showcase/systems?page_size=100'); bizSysData=r.data.list||[]; } catch(e){} } + const item = id ? bizSysData.find(s=>s.id===id) : null; + document.getElementById('modalTitle').textContent = id ? '编辑系统' : '新增系统'; + document.getElementById('modalBody').innerHTML = ` +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`; + editingId = id; + _modalSaveFn = saveBizSys; + openModal(); +} + +async function saveBizSys() { + const body = { + name: document.getElementById('m_name').value, + category: document.getElementById('m_category').value, + description: document.getElementById('m_description').value, + detail: document.getElementById('m_detail').value, + price_type: document.getElementById('m_price_type').value, + price: document.getElementById('m_price').value, + demo_url: document.getElementById('m_demo_url').value, + sort: parseInt(document.getElementById('m_sort').value)||0, + status: parseInt(document.getElementById('m_status').value), + }; + try { + if (editingId) { + await api('/admin/showcase/systems/' + editingId, { method:'PUT', body:JSON.stringify(body) }); + } else { + await api('/admin/showcase/systems', { method:'POST', body:JSON.stringify(body) }); + } + closeModal(); toast('保存成功','success'); renderBizSystems(); + } catch(e) {} +} + +async function deleteBizSys(id) { + if (!confirm('确定删除?')) return; + try { await api('/admin/showcase/systems/'+id,{method:'DELETE'}); toast('删除成功','success'); renderBizSystems(); } catch(e){} +} + // ===== 通用工具 ===== function formatDate(d) { if (!d) return '-'; @@ -1449,7 +1663,12 @@ function renderPagination(d, fnName) { } function showModal() { document.getElementById('modalOverlay').classList.add('show'); } +function openModal() { showModal(); } function closeModal() { document.getElementById('modalOverlay').classList.remove('show'); editingId = null; } +function saveModal() { + if (typeof _modalSaveFn === 'function') _modalSaveFn(); +} +let _modalSaveFn = null; // ===== 初始化 ===== document.getElementById('modalOverlay').addEventListener('click', (e) => { diff --git a/web/dist/index.html b/web/dist/index.html index 8b03592..dbcccc6 100644 --- a/web/dist/index.html +++ b/web/dist/index.html @@ -2,8 +2,53 @@ - -Gotest · 模板导航 + +Gotest · 多行业网站模板集合 - 企业官网/电商/外卖/游戏/应用/音乐模板一站式导航 + + + + + + + + + + + + + + + + + + + + + + + +