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}
+
+
@@ -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 候选码管理
+
+
+
+
+ | ID | 名称 | 分类 | 平台 | 版本 | 评分 | 状态 | 操作 |
+
+ ${d.list.length===0?'| 暂无数据 |
':d.list.map(a=>`
+
+ | ${a.id} |
+ ${a.name} |
+ ${a.category||'-'} |
+ ${a.platform||'-'} |
+ ${a.version||'-'} |
+ ⭐${a.rating} |
+ ${a.status===1?'上架':'下架'} |
+
+
+
+ |
+
`).join('')}
+
+
+
+ ${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 = `
+
+
+
💼 业务系统管理
+
+
+
+
+ | ID | 名称 | 分类 | 价格类型 | 价格 | 状态 | 操作 |
+
+ ${d.list.length===0?'| 暂无数据 |
':d.list.map(s=>`
+
+ | ${s.id} |
+ ${s.name} |
+ ${s.category||'-'} |
+ ${s.price_type||'-'} |
+ ${s.price||'-'} |
+ ${s.status===1?'上线':'下线'} |
+
+
+
+ |
+
`).join('')}
+
+
+
+ ${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 · 多行业网站模板集合 - 企业官网/电商/外卖/游戏/应用/音乐模板一站式导航
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+