diff --git a/go/controllers/index_website.go b/go/controllers/index_website.go index e2cf742..b6df555 100644 --- a/go/controllers/index_website.go +++ b/go/controllers/index_website.go @@ -155,7 +155,9 @@ func (c *IndexWebsiteController) News() { cond := publishedArticleCond() if cateStr := strings.TrimSpace(c.GetString("category")); cateStr != "" { if cid, err := strconv.ParseUint(cateStr, 10, 64); err == nil && cid > 0 { - cond = cond.And("cate_id", cid) + // 选中父分类时把它整棵子树的文章一起返回,否则挂在子分类上的内容会查不到 + ids := append([]uint64{cid}, models.PlatformWebsiteCategoryDescendantMap()[cid]...) + cond = cond.And("cate_id__in", ids) } } if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" { @@ -221,20 +223,25 @@ func (c *IndexWebsiteController) NewsCategories() { if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteCategory)). Filter("delete_time__isnull", true). OrderBy("sort", "id"). - All(&cates, "ID", "Name", "Desc", "Image", "Sort"); err != nil { + All(&cates, "ID", "Cid", "Name", "Desc", "Image", "Sort"); err != nil { c.fail(500, 500, "获取分类失败: "+err.Error()) return } + // 父分类的数量包含其所有后代,与列表页按父分类筛选的口径保持一致 + descMap := models.PlatformWebsiteCategoryDescendantMap() + list := make([]map[string]interface{}, 0, len(cates)) for _, item := range cates { + ids := append([]uint64{item.ID}, descMap[item.ID]...) cnt, _ := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)). Filter("delete_time__isnull", true). Filter("status", articleStatusPublished). - Filter("cate_id", item.ID). + Filter("cate_id__in", ids). Count() list = append(list, map[string]interface{}{ "id": item.ID, + "cid": item.Cid, "name": item.Name, "desc": item.Desc, "image": item.Image, @@ -390,3 +397,89 @@ func (c *IndexWebsiteController) NewsView() { } c.ok(nil) } + +// ============================================================= +// 站点公告 yz_platform_website_notice +// +// GET /site/notices 公告列表(?limit=6&popup=1 只取弹窗公告) +// GET /site/notices/:id 公告详情(正文为 HTML,浏览量 +1) +// +// 仅返回「已发布」且当前时间处于有效期内的公告,条件见 noticeVisibleCond。 +// ============================================================= + +// noticeListItem 公告列表项(不含正文,减小响应体) +func noticeListItem(r models.PlatformWebsiteNotice) map[string]interface{} { + return map[string]interface{}{ + "id": r.ID, + "title": r.Title, + "summary": r.Summary, + "type": r.Type, + "link_url": r.LinkURL, + "is_popup": r.IsPopup, + "top": r.Top, + "date": r.PublishTime, + } +} + +// Notices GET /site/notices?limit=&popup= +func (c *IndexWebsiteController) Notices() { + limit, _ := c.GetInt("limit", 6) + if limit < 1 { + limit = 6 + } + if limit > 50 { + limit = 50 + } + + cond := noticeVisibleCond() + // popup 传 1 / true 时只取弹窗公告(首页弹窗、侧栏公告用) + if pop := strings.TrimSpace(c.GetString("popup")); pop != "" && pop != "0" { + cond = cond.And("is_popup", 1) + } + + var rows []models.PlatformWebsiteNotice + if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteNotice)). + SetCond(cond). + OrderBy("-top", "sort", "-publish_time", "-id"). + Limit(limit). + All(&rows, "ID", "Title", "Summary", "Type", "LinkURL", "IsPopup", "Top", + "Sort", "PublishTime", "CreateTime"); err != nil { + c.fail(500, 500, "获取公告失败: "+err.Error()) + return + } + + list := make([]map[string]interface{}, 0, len(rows)) + for _, r := range rows { + list = append(list, noticeListItem(r)) + } + c.ok(map[string]interface{}{"list": list, "total": len(list)}) +} + +// NoticeDetail GET /site/notices/:id +func (c *IndexWebsiteController) NoticeDetail() { + id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64) + if err != nil || id == 0 { + c.fail(400, 400, "无效的公告ID") + return + } + + var row models.PlatformWebsiteNotice + if err = models.Orm.QueryTable(new(models.PlatformWebsiteNotice)). + SetCond(noticeVisibleCond()). + Filter("id", id). + One(&row); err != nil { + c.fail(404, 404, "公告不存在或已下线") + return + } + + // 浏览量 +1,失败不影响正文展示 + _, _ = models.Orm.Raw( + "UPDATE yz_platform_website_notice SET views = views + 1 WHERE id = ?", + id, + ).Exec() + + item := noticeListItem(row) + item["content"] = row.Content + item["views"] = row.Views + 1 + c.ok(item) +} diff --git a/go/controllers/platform_website_common.go b/go/controllers/platform_website_common.go index 5d9dd95..c6931a1 100644 --- a/go/controllers/platform_website_common.go +++ b/go/controllers/platform_website_common.go @@ -62,9 +62,9 @@ func websiteID(c *beego.Controller) (uint64, bool) { return id, true } -// websiteStatusParam 解析 status 查询参数,未传时返回 -1(表示不过滤) -func websiteStatusParam(c *beego.Controller) int { - raw := strings.TrimSpace(c.GetString("status")) +// websiteIntParam 解析整型查询参数,未传或非法时返回 -1(表示不过滤) +func websiteIntParam(c *beego.Controller, key string) int { + raw := strings.TrimSpace(c.GetString(key)) if raw == "" { return -1 } @@ -75,6 +75,11 @@ func websiteStatusParam(c *beego.Controller) int { return v } +// websiteStatusParam 解析 status 查询参数,未传时返回 -1(表示不过滤) +func websiteStatusParam(c *beego.Controller) int { + return websiteIntParam(c, "status") +} + // websiteBind 解析 JSON 请求体到 target,失败时已写出 400 func websiteBind(c *beego.Controller, target interface{}) bool { body, err := io.ReadAll(c.Ctx.Request.Body) diff --git a/go/controllers/platform_website_notice.go b/go/controllers/platform_website_notice.go new file mode 100644 index 0000000..0db293b --- /dev/null +++ b/go/controllers/platform_website_notice.go @@ -0,0 +1,426 @@ +package controllers + +import ( + "strings" + "time" + + "server/models" + "server/pkg/jwtutil" + + beego "github.com/beego/beego/v2/server/web" + "github.com/beego/beego/v2/client/orm" +) + +// ============================================================= +// 平台官网 - 站点公告 /platform/website/notice/* +// 表:yz_platform_website_notice +// 建表 SQL:docs/sql/create_platform_website_notice.sql +// +// 公告状态:0 草稿 / 1 已发布 / 2 已下线。 +// 前台 GET /site/notices 只返回「已发布」且当前时间处于有效期内的公告, +// 见 index_website.go 的 noticeVisibleCond。 +// ============================================================= + +const ( + noticeStatusDraft int8 = 0 + noticeStatusPublished int8 = 1 + noticeStatusOffline int8 = 2 +) + +type PlatformWebsiteNoticeController struct { + beego.Controller +} + +type platformWebsiteNoticePayload struct { + Title *string `json:"title"` + Summary *string `json:"summary"` + Content *string `json:"content"` + Type *int8 `json:"type"` + LinkURL *string `json:"link_url"` + IsPopup *int8 `json:"is_popup"` + Top *int8 `json:"top"` + Sort *int `json:"sort"` + Status *int8 `json:"status"` + StartTime *string `json:"start_time"` + EndTime *string `json:"end_time"` +} + +// parseNoticeTime 解析前端时间字符串,支持 "2006-01-02 15:04:05"、RFC3339、"2006-01-02"; +// 空串或无法识别时返回 nil(对应数据库 NULL,表示不限制时间)。 +func parseNoticeTime(raw *string) *time.Time { + if raw == nil { + return nil + } + v := strings.TrimSpace(*raw) + if v == "" { + return nil + } + for _, layout := range []string{"2006-01-02 15:04:05", time.RFC3339, "2006-01-02"} { + if t, err := time.ParseInLocation(layout, v, time.Local); err == nil { + return &t + } + } + return nil +} + +// noticeVisibleCond 前台可见条件:未删除 + 已发布 + 当前时间落在 [start_time, end_time] 内 +// start_time / end_time 为 NULL 表示该侧不限制。 +func noticeVisibleCond() *orm.Condition { + now := time.Now() + return orm.NewCondition(). + And("delete_time__isnull", true). + And("status", noticeStatusPublished). + AndCond(orm.NewCondition().Or("start_time__isnull", true).Or("start_time__lte", now)). + AndCond(orm.NewCondition().Or("end_time__isnull", true).Or("end_time__gte", now)) +} + +// noticeFromPayload 依据请求体构造新公告,缺省:类型=通知公告、状态=草稿 +func noticeFromPayload(p platformWebsiteNoticePayload) models.PlatformWebsiteNotice { + row := models.PlatformWebsiteNotice{ + Type: 1, + Status: noticeStatusDraft, + } + if p.Title != nil { + row.Title = strings.TrimSpace(*p.Title) + } + if p.Summary != nil { + row.Summary = strings.TrimSpace(*p.Summary) + } + if p.Content != nil { + row.Content = *p.Content + } + if p.Type != nil { + row.Type = *p.Type + } + if p.LinkURL != nil { + row.LinkURL = strings.TrimSpace(*p.LinkURL) + } + if p.IsPopup != nil { + row.IsPopup = *p.IsPopup + } + if p.Top != nil { + row.Top = *p.Top + } + if p.Sort != nil { + row.Sort = *p.Sort + } + if p.Status != nil { + row.Status = *p.Status + } + row.StartTime = parseNoticeTime(p.StartTime) + row.EndTime = parseNoticeTime(p.EndTime) + return row +} + +// noticeUpdateMap 把请求体转成待更新字段,只包含显式传入的键(未传的字段保持原值) +func noticeUpdateMap(p platformWebsiteNoticePayload) map[string]interface{} { + up := map[string]interface{}{} + if p.Title != nil { + up["title"] = strings.TrimSpace(*p.Title) + } + if p.Summary != nil { + up["summary"] = strings.TrimSpace(*p.Summary) + } + if p.Content != nil { + up["content"] = *p.Content + } + if p.Type != nil { + up["type"] = *p.Type + } + if p.LinkURL != nil { + up["link_url"] = strings.TrimSpace(*p.LinkURL) + } + if p.IsPopup != nil { + up["is_popup"] = *p.IsPopup + } + if p.Top != nil { + up["top"] = *p.Top + } + if p.Sort != nil { + up["sort"] = *p.Sort + } + if p.Status != nil { + up["status"] = *p.Status + } + if p.StartTime != nil { + up["start_time"] = parseNoticeTime(p.StartTime) + } + if p.EndTime != nil { + up["end_time"] = parseNoticeTime(p.EndTime) + } + return up +} + +// List GET /platform/website/notice/list +// query: keyword(标题/摘要)、type、status、is_popup、page、pageSize +func (c *PlatformWebsiteNoticeController) List() { + if _, ok := websiteAuth(&c.Controller); !ok { + return + } + page, pageSize := websitePaging(&c.Controller) + + cond := orm.NewCondition().And("delete_time__isnull", true) + if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" { + cond = cond.AndCond(orm.NewCondition(). + Or("title__icontains", kw). + Or("summary__icontains", kw)) + } + if tp := websiteIntParam(&c.Controller, "type"); tp >= 0 { + cond = cond.And("type", tp) + } + if st := websiteStatusParam(&c.Controller); st >= 0 { + cond = cond.And("status", st) + } + if pop := websiteIntParam(&c.Controller, "is_popup"); pop >= 0 { + cond = cond.And("is_popup", pop) + } + + qs := models.Orm.QueryTable(new(models.PlatformWebsiteNotice)).SetCond(cond) + total, err := qs.Count() + if err != nil { + jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error()) + return + } + // 列表不返回富文本正文,减小响应体 + var rows []models.PlatformWebsiteNotice + if _, err = qs.OrderBy("-top", "sort", "-id"). + Limit(pageSize, (page-1)*pageSize). + All(&rows, "ID", "Title", "Summary", "Type", "LinkURL", "IsPopup", "Top", + "Sort", "Status", "StartTime", "EndTime", "Views", + "PublishTime", "CreateTime", "UpdateTime"); err != nil { + jsonErr(&c.Controller, 500, 500, "获取失败: "+err.Error()) + return + } + websiteOK(&c.Controller, map[string]interface{}{"list": rows, "total": total}) +} + +// Get GET /platform/website/notice/:id — 含正文,供编辑回显 +func (c *PlatformWebsiteNoticeController) Get() { + if _, ok := websiteAuth(&c.Controller); !ok { + return + } + id, ok := websiteID(&c.Controller) + if !ok { + return + } + var row models.PlatformWebsiteNotice + if err := models.Orm.QueryTable(new(models.PlatformWebsiteNotice)). + Filter("id", id). + Filter("delete_time__isnull", true). + One(&row); err != nil { + jsonErr(&c.Controller, 404, 404, "公告不存在") + return + } + websiteOK(&c.Controller, row) +} + +// Create POST /platform/website/notice +func (c *PlatformWebsiteNoticeController) Create() { + claims, ok := websiteAuth(&c.Controller) + if !ok { + return + } + var p platformWebsiteNoticePayload + if !websiteBind(&c.Controller, &p) { + return + } + if p.Title == nil || strings.TrimSpace(*p.Title) == "" { + jsonErr(&c.Controller, 400, 400, "公告标题不能为空") + return + } + + row := noticeFromPayload(p) + // 直接提交为已发布时同步补发布时间与发布人 + if row.Status == noticeStatusPublished { + now := time.Now() + uid := uint64(claims.UserID) + row.PublishTime = &now + row.PublisherID = &uid + } + + id, err := models.Orm.Insert(&row) + if err != nil { + jsonErr(&c.Controller, 500, 500, "创建失败: "+err.Error()) + return + } + websiteOK(&c.Controller, map[string]interface{}{"id": id}) +} + +// Update POST /platform/website/notice/:id +func (c *PlatformWebsiteNoticeController) Update() { + claims, ok := websiteAuth(&c.Controller) + if !ok { + return + } + id, ok := websiteID(&c.Controller) + if !ok { + return + } + var p platformWebsiteNoticePayload + if !websiteBind(&c.Controller, &p) { + return + } + if p.Title != nil && strings.TrimSpace(*p.Title) == "" { + jsonErr(&c.Controller, 400, 400, "公告标题不能为空") + return + } + + // 先取原记录:既用于 404 判断,也用于判断是否「首次转为已发布」 + var row models.PlatformWebsiteNotice + if err := models.Orm.QueryTable(new(models.PlatformWebsiteNotice)). + Filter("id", id). + Filter("delete_time__isnull", true). + One(&row, "ID", "Status"); err != nil { + jsonErr(&c.Controller, 404, 404, "公告不存在") + return + } + + up := noticeUpdateMap(p) + if len(up) == 0 { + jsonErr(&c.Controller, 400, 400, "无更新字段") + return + } + if p.Status != nil && *p.Status == noticeStatusPublished && row.Status != noticeStatusPublished { + // 首次发布才写发布时间,避免每次保存覆盖发布时间 + now := time.Now() + uid := uint64(claims.UserID) + up["publish_time"] = now + up["publisher_id"] = uid + } + up["update_time"] = time.Now() + + if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteNotice)). + Filter("id", id). + Filter("delete_time__isnull", true). + Update(up); err != nil { + jsonErr(&c.Controller, 500, 500, "更新失败: "+err.Error()) + return + } + websiteOK(&c.Controller, nil) +} + +// noticeToggle 更新单个开关字段(status / top / is_popup) +func (c *PlatformWebsiteNoticeController) noticeToggle(claims *jwtutil.Claims, field string, value int8) { + id, ok := websiteID(&c.Controller) + if !ok { + return + } + up := map[string]interface{}{field: value, "update_time": time.Now()} + // 发布时补发布时间与发布人(其余开关无需登录身份,claims 可能为 nil) + if field == "status" && value == noticeStatusPublished { + up["publish_time"] = time.Now() + if claims != nil { + up["publisher_id"] = uint64(claims.UserID) + } + } + n, err := models.Orm.QueryTable(new(models.PlatformWebsiteNotice)). + Filter("id", id). + Filter("delete_time__isnull", true). + Update(up) + if err != nil { + jsonErr(&c.Controller, 500, 500, "操作失败: "+err.Error()) + return + } + if n == 0 { + jsonErr(&c.Controller, 404, 404, "公告不存在") + return + } + websiteOK(&c.Controller, nil) +} + +// Publish POST /platform/website/notice/:id/publish — 状态置为已发布 +func (c *PlatformWebsiteNoticeController) Publish() { + claims, ok := websiteAuth(&c.Controller) + if !ok { + return + } + c.noticeToggle(claims, "status", noticeStatusPublished) +} + +// UnPublish POST /platform/website/notice/:id/unpublish — 状态置为已下线 +func (c *PlatformWebsiteNoticeController) UnPublish() { + if _, ok := websiteAuth(&c.Controller); !ok { + return + } + c.noticeToggle(nil, "status", noticeStatusOffline) +} + +// Top POST /platform/website/notice/:id/top +func (c *PlatformWebsiteNoticeController) Top() { + if _, ok := websiteAuth(&c.Controller); !ok { + return + } + c.noticeToggle(nil, "top", 1) +} + +// UnTop POST /platform/website/notice/:id/untop +func (c *PlatformWebsiteNoticeController) UnTop() { + if _, ok := websiteAuth(&c.Controller); !ok { + return + } + c.noticeToggle(nil, "top", 0) +} + +// Popup POST /platform/website/notice/:id/popup — 标记为弹窗公告 +func (c *PlatformWebsiteNoticeController) Popup() { + if _, ok := websiteAuth(&c.Controller); !ok { + return + } + c.noticeToggle(nil, "is_popup", 1) +} + +// UnPopup POST /platform/website/notice/:id/unpopup +func (c *PlatformWebsiteNoticeController) UnPopup() { + if _, ok := websiteAuth(&c.Controller); !ok { + return + } + c.noticeToggle(nil, "is_popup", 0) +} + +// Delete DELETE /platform/website/notice/:id — 软删除 +func (c *PlatformWebsiteNoticeController) Delete() { + if _, ok := websiteAuth(&c.Controller); !ok { + return + } + id, ok := websiteID(&c.Controller) + if !ok { + return + } + now := time.Now() + n, err := models.Orm.QueryTable(new(models.PlatformWebsiteNotice)). + Filter("id", id). + Filter("delete_time__isnull", true). + Update(map[string]interface{}{"delete_time": now, "update_time": now}) + if err != nil { + jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + if n == 0 { + jsonErr(&c.Controller, 404, 404, "公告不存在") + return + } + websiteOK(&c.Controller, nil) +} + +// BatchDelete POST /platform/website/notice/batchdelete — body { ids: [] } +func (c *PlatformWebsiteNoticeController) BatchDelete() { + if _, ok := websiteAuth(&c.Controller); !ok { + return + } + var p struct { + IDs []uint64 `json:"ids"` + } + if !websiteBind(&c.Controller, &p) || len(p.IDs) == 0 { + jsonErr(&c.Controller, 400, 400, "请选择要删除的公告") + return + } + now := time.Now() + if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteNotice)). + Filter("id__in", p.IDs). + Filter("delete_time__isnull", true). + Update(map[string]interface{}{"delete_time": now, "update_time": now}); err != nil { + jsonErr(&c.Controller, 500, 500, "删除失败: "+err.Error()) + return + } + websiteOK(&c.Controller, nil) +} diff --git a/go/docs/sql/create_platform_website_notice.sql b/go/docs/sql/create_platform_website_notice.sql new file mode 100644 index 0000000..b6c4f6d --- /dev/null +++ b/go/docs/sql/create_platform_website_notice.sql @@ -0,0 +1,88 @@ +-- ============================================================= +-- 平台官网 - 站点公告 +-- 表:yz_platform_website_notice +-- +-- 用途:平台端「官网管理 → 内容管理 → 站点公告」维护官网的通知 / 活动 / +-- 系统维护类公告;前台经 GET /site/notices 读取(仅返回「已发布」 +-- 且处于有效期内的公告)。 +-- +-- 执行方式:人工在数据库执行(代码中不做任何自动建表 / 补列)。 +-- 说明:下方 DDL 幂等,可重复执行。 +-- ============================================================= + +CREATE TABLE IF NOT EXISTS `yz_platform_website_notice` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `title` varchar(255) NOT NULL DEFAULT '' COMMENT '公告标题', + `summary` varchar(500) NOT NULL DEFAULT '' COMMENT '公告摘要(前台卡片展示,留空则前台截取正文)', + `content` mediumtext COMMENT '公告正文(富文本 HTML)', + `type` tinyint NOT NULL DEFAULT 1 COMMENT '公告类型:1 通知公告 2 活动公告 3 系统维护', + `link_url` varchar(500) NOT NULL DEFAULT '' COMMENT '点击跳转地址,站内相对路径或完整 URL;留空表示不可点击', + `is_popup` tinyint NOT NULL DEFAULT 0 COMMENT '是否作为弹窗/侧栏公告:0 否 1 是', + `top` tinyint NOT NULL DEFAULT 0 COMMENT '是否置顶:0 否 1 是', + `sort` int NOT NULL DEFAULT 0 COMMENT '排序,数字越小越靠前', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0 草稿 1 已发布 2 已下线', + `start_time` datetime DEFAULT NULL COMMENT '生效开始时间,NULL 表示立即生效', + `end_time` datetime DEFAULT NULL COMMENT '生效结束时间,NULL 表示永久有效', + `views` int NOT NULL DEFAULT 0 COMMENT '浏览量', + `publisher_id` bigint UNSIGNED DEFAULT NULL COMMENT '发布人(平台用户ID)', + `publish_time` datetime DEFAULT NULL COMMENT '发布时间', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + `delete_time` datetime DEFAULT NULL COMMENT '删除时间(软删除)', + PRIMARY KEY (`id`), + KEY `idx_status_sort` (`status`, `sort`, `id`), + KEY `idx_popup` (`is_popup`, `status`), + KEY `idx_publish_time` (`publish_time`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '平台官网-站点公告'; + + +-- ============================================================= +-- 平台菜单:把「站点公告」挂到「官网管理 → 内容管理」下 +-- +-- 菜单数据统一维护在 yz_system_menu(平台端 cid=1 的角色可见)。 +-- 父级不用写死自增ID,按现有「内容管理」的子菜单反查,可在任意环境重复执行。 +-- 若本次不需要自动挂菜单,可只执行上面的建表语句,手动在 +-- 「系统管理 → 菜单管理」里添加:路径 /website/content/notice, +-- 组件 /website/content/notice/index.vue +-- ============================================================= + +-- 父级定位一:取现有「内容管理」子菜单(文章管理)的 pid +SET @notice_pid := ( + SELECT `pid` FROM `yz_system_menu` + WHERE `delete_time` IS NULL AND `component_path` LIKE '/website/content/%' + ORDER BY `id` ASC LIMIT 1 +); + +-- 父级定位二:兜底按「内容管理」的路径或名称查 +SET @notice_pid := IFNULL(@notice_pid, ( + SELECT `id` FROM `yz_system_menu` + WHERE `delete_time` IS NULL + AND (`path` = '/website/content' OR `title` = '内容管理') + ORDER BY `id` ASC LIMIT 1 +)); + +-- 已存在同路径菜单时跳过,避免重复插入 +INSERT INTO `yz_system_menu` + (`pid`, `title`, `path`, `component_path`, `icon`, `sort`, `status`, `is_visible`, `views`, `type`, `creater`, `remark`, `create_time`) +SELECT + @notice_pid, '站点公告', '/website/content/notice', '/website/content/notice/index.vue', + 'Bell', 50, 1, 1, '[1]', 2, 'sql', '官网管理 - 站点公告', NOW() +FROM (SELECT 1) AS `t` +WHERE @notice_pid IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM ( + SELECT `id` FROM `yz_system_menu` + WHERE `delete_time` IS NULL AND `path` = '/website/content/notice' + LIMIT 1 + ) AS `exist` + ); + + +-- ============================================================= +-- 可选:一条示例公告,便于前台联调(不需要可整段跳过) +-- ============================================================= +INSERT INTO `yz_platform_website_notice` + (`title`, `summary`, `content`, `type`, `is_popup`, `top`, `sort`, `status`, `publish_time`) +VALUES + ('云泽科技官网全新改版上线', '本次改版聚焦信息架构与访问体验,欢迎反馈建议。', + '
云泽科技官网全新改版上线,本次改版聚焦信息架构与访问体验。
', 1, 1, 1, 0, 1, NOW()); diff --git a/go/models/init.go b/go/models/init.go index 73c93c0..f873cf9 100644 --- a/go/models/init.go +++ b/go/models/init.go @@ -129,6 +129,8 @@ func Init(_ string) { new(PlatformWebsiteNav), new(PlatformWebsiteLink), new(PlatformWebsitePage), + // 站点公告(docs/sql/create_platform_website_notice.sql) + new(PlatformWebsiteNotice), new(SystemReminderList), diff --git a/go/models/platform_website.go b/go/models/platform_website.go index 2f338f7..30bb902 100644 --- a/go/models/platform_website.go +++ b/go/models/platform_website.go @@ -160,6 +160,33 @@ func (m *PlatformWebsitePage) TableName() string { return "yz_platform_website_page" } +// PlatformWebsiteNotice 平台官网-站点公告 yz_platform_website_notice +// 建表 SQL:docs/sql/create_platform_website_notice.sql +type PlatformWebsiteNotice struct { + ID uint64 `orm:"column(id);pk;auto" json:"id"` + Title string `orm:"column(title);size(255);default('')" json:"title"` + Summary string `orm:"column(summary);size(500);default('')" json:"summary"` + Content string `orm:"column(content);type(mediumtext);null" json:"content"` + Type int8 `orm:"column(type);default(1)" json:"type"` // 1 通知公告 2 活动公告 3 系统维护 + LinkURL string `orm:"column(link_url);size(500);default('')" json:"link_url"` + IsPopup int8 `orm:"column(is_popup);default(0)" json:"is_popup"` // 是否作为弹窗公告 + Top int8 `orm:"column(top);default(0)" json:"top"` // 是否置顶 + Sort int `orm:"column(sort);default(0)" json:"sort"` + Status int8 `orm:"column(status);default(0)" json:"status"` // 0 草稿 1 已发布 2 已下线 + StartTime *time.Time `orm:"column(start_time);type(datetime);null" json:"start_time"` + EndTime *time.Time `orm:"column(end_time);type(datetime);null" json:"end_time"` + Views int `orm:"column(views);default(0)" json:"views"` + PublisherID *uint64 `orm:"column(publisher_id);null" json:"publisher_id"` + PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"` + CreateTime time.Time `orm:"column(create_time);type(datetime);auto_now_add" json:"create_time"` + UpdateTime *time.Time `orm:"column(update_time);type(datetime);null" json:"update_time"` + DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"` +} + +func (m *PlatformWebsiteNotice) TableName() string { + return "yz_platform_website_notice" +} + // ===================== 公共查询helper ===================== // PlatformWebsiteCategoryNameMap 分类ID -> 分类名称 @@ -199,6 +226,50 @@ func PlatformWebsiteCategoryMap(ids []uint64) map[uint64]PlatformWebsiteCategory return out } +// PlatformWebsiteCategoryDescendantMap 分类ID -> 全部后代分类ID(不含自身) +// +// 官网前台按父分类筛选 / 统计数量时,需要把整棵子树都算进来: +// 文章挂在子分类上时,点父分类也应该能看到。 +// 分类数据量很小,每次整体构建一次即可;存在环时靠 seen 截断。 +func PlatformWebsiteCategoryDescendantMap() map[uint64][]uint64 { + out := make(map[uint64][]uint64) + + var rows []PlatformWebsiteCategory + if _, err := Orm.QueryTable(new(PlatformWebsiteCategory)). + Filter("delete_time__isnull", true). + All(&rows, "ID", "Cid"); err != nil { + return out + } + + children := make(map[uint64][]uint64, len(rows)) + ids := make([]uint64, 0, len(rows)) + for _, r := range rows { + ids = append(ids, r.ID) + if r.Cid > 0 && r.Cid != r.ID { + children[r.Cid] = append(children[r.Cid], r.ID) + } + } + + var walk func(id uint64, seen map[uint64]bool) []uint64 + walk = func(id uint64, seen map[uint64]bool) []uint64 { + res := make([]uint64, 0, len(children[id])) + for _, child := range children[id] { + if seen[child] { + continue + } + seen[child] = true + res = append(res, child) + res = append(res, walk(child, seen)...) + } + return res + } + + for _, id := range ids { + out[id] = walk(id, map[uint64]bool{id: true}) + } + return out +} + // PlatformWebsiteArticleTitleMap 文章ID -> 标题(评论列表展示所属文章用) func PlatformWebsiteArticleTitleMap(ids []uint64) map[uint64]string { out := make(map[uint64]string) diff --git a/go/routers/index/index.go b/go/routers/index/index.go index 109bc9d..c62f8d6 100644 --- a/go/routers/index/index.go +++ b/go/routers/index/index.go @@ -28,6 +28,10 @@ func Register() { beego.Router("/site/news/:id/view", &controllers.IndexWebsiteController{}, "post:NewsView") beego.Router("/site/news/:id", &controllers.IndexWebsiteController{}, "get:NewsDetail") + // 官网前台站点公告(平台「官网管理 - 站点公告」配置,仅返回已发布且在有效期内的) + beego.Router("/site/notices", &controllers.IndexWebsiteController{}, "get:Notices") + beego.Router("/site/notices/:id", &controllers.IndexWebsiteController{}, "get:NoticeDetail") + beego.Router("/index/headmenu", &controllers.IndexPortalController{}, "get:GetHeadMenu") beego.Router("/index/footerdata", &controllers.IndexPortalController{}, "get:GetFooterData") beego.Router("/index/newscentertop4", &controllers.IndexPortalController{}, "get:GetNewsCenterTop4") diff --git a/go/routers/platform/platform.go b/go/routers/platform/platform.go index d91a087..53431a4 100644 --- a/go/routers/platform/platform.go +++ b/go/routers/platform/platform.go @@ -402,4 +402,17 @@ func Register() { beego.Router("/platform/website/page/all", &controllers.PlatformWebsitePageController{}, "get:All") beego.Router("/platform/website/page", &controllers.PlatformWebsitePageController{}, "post:Create") beego.Router("/platform/website/page/:id", &controllers.PlatformWebsitePageController{}, "get:Get;post:Update;delete:Delete") + + // 站点公告 yz_platform_website_notice + // 注意:静态段(list / batchdelete)必须在 :id 之前注册 + beego.Router("/platform/website/notice/list", &controllers.PlatformWebsiteNoticeController{}, "get:List") + beego.Router("/platform/website/notice/batchdelete", &controllers.PlatformWebsiteNoticeController{}, "post:BatchDelete") + beego.Router("/platform/website/notice", &controllers.PlatformWebsiteNoticeController{}, "post:Create") + beego.Router("/platform/website/notice/:id/publish", &controllers.PlatformWebsiteNoticeController{}, "post:Publish") + beego.Router("/platform/website/notice/:id/unpublish", &controllers.PlatformWebsiteNoticeController{}, "post:UnPublish") + beego.Router("/platform/website/notice/:id/top", &controllers.PlatformWebsiteNoticeController{}, "post:Top") + beego.Router("/platform/website/notice/:id/untop", &controllers.PlatformWebsiteNoticeController{}, "post:UnTop") + beego.Router("/platform/website/notice/:id/popup", &controllers.PlatformWebsiteNoticeController{}, "post:Popup") + beego.Router("/platform/website/notice/:id/unpopup", &controllers.PlatformWebsiteNoticeController{}, "post:UnPopup") + beego.Router("/platform/website/notice/:id", &controllers.PlatformWebsiteNoticeController{}, "get:Get;post:Update;delete:Delete") } diff --git a/platform/src/api/notice.js b/platform/src/api/notice.js new file mode 100644 index 0000000..fc918a9 --- /dev/null +++ b/platform/src/api/notice.js @@ -0,0 +1,105 @@ +import request from "@/utils/request"; + +// ==================== 官网管理 - 站点公告(平台端) ==================== +// 数据表:yz_platform_website_notice +// 建表 SQL:go/docs/sql/create_platform_website_notice.sql +// +// 状态约定:0 草稿 / 1 已发布 / 2 已下线 +// 类型约定:1 通知公告 / 2 活动公告 / 3 系统维护 + +// 公告列表(分页):keyword / type / status / is_popup / page / pageSize +export function listNotices(params) { + return request({ + url: "/platform/website/notice/list", + method: "get", + params, + }); +} + +// 公告详情(含富文本正文,供编辑回显) +export function getNotice(id) { + return request({ + url: `/platform/website/notice/${id}`, + method: "get", + }); +} + +// 新增公告 +export function createNotice(data) { + return request({ + url: "/platform/website/notice", + method: "post", + data, + }); +} + +// 编辑公告 +export function editNotice(id, data) { + return request({ + url: `/platform/website/notice/${id}`, + method: "post", + data, + }); +} + +// 删除公告(软删除) +export function deleteNotice(id) { + return request({ + url: `/platform/website/notice/${id}`, + method: "delete", + }); +} + +// 批量删除公告 +export function batchDeleteNotices(ids) { + return request({ + url: "/platform/website/notice/batchdelete", + method: "post", + data: { ids }, + }); +} + +// 发布 / 下线(status 1 / 2) +export function publishNotice(id) { + return request({ + url: `/platform/website/notice/${id}/publish`, + method: "post", + }); +} + +export function unpublishNotice(id) { + return request({ + url: `/platform/website/notice/${id}/unpublish`, + method: "post", + }); +} + +// 置顶 / 取消置顶 +export function topNotice(id) { + return request({ + url: `/platform/website/notice/${id}/top`, + method: "post", + }); +} + +export function untopNotice(id) { + return request({ + url: `/platform/website/notice/${id}/untop`, + method: "post", + }); +} + +// 标记为弹窗公告 / 取消弹窗 +export function popupNotice(id) { + return request({ + url: `/platform/website/notice/${id}/popup`, + method: "post", + }); +} + +export function unpopupNotice(id) { + return request({ + url: `/platform/website/notice/${id}/unpopup`, + method: "post", + }); +} diff --git a/platform/src/views/website/content/notice/components/edit.vue b/platform/src/views/website/content/notice/components/edit.vue new file mode 100644 index 0000000..7aac7e6 --- /dev/null +++ b/platform/src/views/website/content/notice/components/edit.vue @@ -0,0 +1,291 @@ + +GET /site/notices(?popup=1 只取弹窗公告)。
+