Files
yunzerwebsiteallinone/go/controllers/index_website.go
T
2026-09-17 23:42:41 +08:00

486 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controllers
import (
"strconv"
"strings"
"server/models"
beego "github.com/beego/beego/v2/server/web"
"github.com/beego/beego/v2/client/orm"
)
// =============================================================
// 官网前台只读接口:平台官网内容(yz_platform_website_*)
//
// 供 website SPA 调用,无需登录(与 /site/setting 同级)。
// 数据由 platform 端「官网管理」模块维护;仅返回「已发布」的内容。
//
// GET /site/news 新闻列表(分页 / 分类 / 关键词)
// GET /site/news/categories 新闻分类 + 各分类已发布数量
// GET /site/news/:id 新闻详情(正文为 HTML)
// GET /site/news/:id/related 相关推荐
// POST /site/news/:id/view 浏览量 +1
//
// 说明:列表与详情中返回的 image 形如 /uploads/xxx,由前端拼接接口域名。
// =============================================================
// 文章状态:与 platform 端「文章管理」保持一致
const (
articleStatusPublished int8 = 2
)
// 导航菜单类型:与 platform 端「导航菜单」保持一致
const (
navTypeExternal int8 = 3
)
type IndexWebsiteController struct {
beego.Controller
}
func (c *IndexWebsiteController) ok(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
func (c *IndexWebsiteController) fail(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
// publishedArticleQuery 已发布文章的基础查询条件
func publishedArticleCond() *orm.Condition {
return orm.NewCondition().
And("delete_time__isnull", true).
And("status", articleStatusPublished)
}
// articleListItem 列表项(不含正文,减小响应体)
// image 与 category_image 分开返回,由前端决定兜底:文章没有封面时用所属分类的图片。
func articleListItem(row models.PlatformWebsiteArticle, cate models.PlatformWebsiteCategory, tags []string) map[string]interface{} {
return map[string]interface{}{
"id": row.ID,
"title": row.Title,
"summary": row.Desc,
"image": row.Image,
"author": row.Author,
"views": row.Views,
"top": row.Top,
"recommend": row.Recommend,
"date": row.PublishTime,
"category_id": row.CateID,
"category_name": cate.Name,
"category_desc": cate.Desc,
"category_image": cate.Image,
"tags": tags,
}
}
// navNode 导航菜单节点(递归结构,供页头渲染)
type navNode struct {
ID uint64 `json:"id"`
Pid uint64 `json:"pid"`
Title string `json:"title"`
Type int8 `json:"type"` // 1 目录 / 2 页面 / 3 外链 / 4 单页
Path string `json:"path"`
Icon string `json:"icon"`
External bool `json:"external"`
Children []*navNode `json:"children"`
}
// Nav GET /site/nav
// 官网页头导航:仅返回启用且显示中的菜单项,按 pid 组树。
// 父级被隐藏/禁用时其子项一并隐藏,避免出现无归属的顶层菜单。
func (c *IndexWebsiteController) Nav() {
var rows []models.PlatformWebsiteNav
if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteNav)).
Filter("delete_time__isnull", true).
Filter("status", 1).
Filter("is_visible", 1).
OrderBy("sort", "id").
All(&rows, "ID", "Pid", "Title", "Type", "Path", "Icon"); err != nil {
c.fail(500, 500, "获取导航菜单失败: "+err.Error())
return
}
nodes := make(map[uint64]*navNode, len(rows))
order := make([]uint64, 0, len(rows))
for _, r := range rows {
nodes[r.ID] = &navNode{
ID: r.ID,
Pid: r.Pid,
Title: r.Title,
Type: r.Type,
Path: r.Path,
Icon: r.Icon,
External: r.Type == navTypeExternal || strings.HasPrefix(r.Path, "http://") || strings.HasPrefix(r.Path, "https://"),
Children: []*navNode{},
}
order = append(order, r.ID)
}
roots := make([]*navNode, 0, len(rows))
for _, id := range order {
node := nodes[id]
if node.Pid == 0 {
roots = append(roots, node)
continue
}
parent, ok := nodes[node.Pid]
if !ok {
continue
}
parent.Children = append(parent.Children, node)
}
c.ok(map[string]interface{}{"list": roots})
}
// News GET /site/news?page=&pageSize=&category=&keyword=
func (c *IndexWebsiteController) News() {
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 6)
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 6
}
if pageSize > 50 {
pageSize = 50
}
cond := publishedArticleCond()
if cateStr := strings.TrimSpace(c.GetString("category")); cateStr != "" {
if cid, err := strconv.ParseUint(cateStr, 10, 64); err == nil && cid > 0 {
// 选中父分类时把它整棵子树的文章一起返回,否则挂在子分类上的内容会查不到
ids := append([]uint64{cid}, models.PlatformWebsiteCategoryDescendantMap()[cid]...)
cond = cond.And("cate_id__in", ids)
}
}
if kw := strings.TrimSpace(c.GetString("keyword")); kw != "" {
cond = cond.AndCond(orm.NewCondition().
Or("title__icontains", kw).
Or("desc__icontains", kw))
}
qs := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).SetCond(cond)
total, err := qs.Count()
if err != nil {
c.fail(500, 500, "获取新闻列表失败: "+err.Error())
return
}
var rows []models.PlatformWebsiteArticle
if _, err = qs.OrderBy("-top", "-recommend", "-publish_time", "-id").
Limit(pageSize, (page-1)*pageSize).
All(&rows, "ID", "CateID", "Title", "Author", "Desc", "Image",
"Status", "Top", "Recommend", "Views", "PublishTime"); err != nil {
c.fail(500, 500, "获取新闻列表失败: "+err.Error())
return
}
articleIDs := make([]uint64, 0, len(rows))
cateIDs := make([]uint64, 0, len(rows))
for _, r := range rows {
articleIDs = append(articleIDs, r.ID)
if r.CateID > 0 {
cateIDs = append(cateIDs, r.CateID)
}
}
cateMap := models.PlatformWebsiteCategoryMap(cateIDs)
tagMap := models.PlatformWebsiteArticleTagNameMap(articleIDs)
list := make([]map[string]interface{}, 0, len(rows))
for _, r := range rows {
tags := tagMap[r.ID]
if tags == nil {
tags = []string{}
}
list = append(list, articleListItem(r, cateMap[r.CateID], tags))
}
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
if totalPages < 1 {
totalPages = 1
}
c.ok(map[string]interface{}{
"list": list,
"total": total,
"page": page,
"pageSize": pageSize,
"totalPages": totalPages,
})
}
// NewsCategories GET /site/news/categories
// 返回启用中的分类及各自已发布文章数量,供列表页筛选条展示。
func (c *IndexWebsiteController) NewsCategories() {
var cates []models.PlatformWebsiteCategory
if _, err := models.Orm.QueryTable(new(models.PlatformWebsiteCategory)).
Filter("delete_time__isnull", true).
OrderBy("sort", "id").
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__in", ids).
Count()
list = append(list, map[string]interface{}{
"id": item.ID,
"cid": item.Cid,
"name": item.Name,
"desc": item.Desc,
"image": item.Image,
"count": cnt,
})
}
total, _ := models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
SetCond(publishedArticleCond()).
Count()
c.ok(map[string]interface{}{"list": list, "total": total})
}
// NewsDetail GET /site/news/:id
func (c *IndexWebsiteController) NewsDetail() {
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.PlatformWebsiteArticle
if err = models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
SetCond(publishedArticleCond()).
Filter("id", id).
One(&row); err != nil {
c.fail(404, 404, "内容不存在或已下线")
return
}
cate := models.PlatformWebsiteCategoryMap([]uint64{row.CateID})[row.CateID]
tags := models.PlatformWebsiteArticleTagNameMap([]uint64{row.ID})[row.ID]
if tags == nil {
tags = []string{}
}
c.ok(map[string]interface{}{
"id": row.ID,
"title": row.Title,
"summary": row.Desc,
"image": row.Image,
"content": row.Content,
"author": row.Author,
"views": row.Views,
"date": row.PublishTime,
"category_id": row.CateID,
"category_name": cate.Name,
"category_desc": cate.Desc,
"category_image": cate.Image,
"tags": tags,
})
}
// NewsRelated GET /site/news/:id/related?limit=3
// 同分类优先,不足时用其它分类补齐。
func (c *IndexWebsiteController) NewsRelated() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.fail(400, 400, "无效的内容ID")
return
}
limit, _ := c.GetInt("limit", 3)
if limit < 1 || limit > 12 {
limit = 3
}
var current models.PlatformWebsiteArticle
if err = models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
SetCond(publishedArticleCond()).
Filter("id", id).
One(&current); err != nil {
// 当前内容不存在时返回空列表,由前端展示"暂无推荐"
c.ok(map[string]interface{}{"list": []interface{}{}})
return
}
pick := make([]models.PlatformWebsiteArticle, 0, limit)
seen := map[uint64]bool{id: true}
// 1. 同分类
if current.CateID > 0 {
var same []models.PlatformWebsiteArticle
_, _ = models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
SetCond(publishedArticleCond()).
Filter("cate_id", current.CateID).
Exclude("id", id).
OrderBy("-publish_time", "-id").
Limit(limit).
All(&same, "ID", "CateID", "Title", "Author", "Desc", "Image",
"Top", "Recommend", "Views", "PublishTime")
for _, r := range same {
if len(pick) >= limit {
break
}
pick = append(pick, r)
seen[r.ID] = true
}
}
// 2. 不足时用最新其它文章补齐
if len(pick) < limit {
ids := make([]uint64, 0, len(seen))
for k := range seen {
ids = append(ids, k)
}
var others []models.PlatformWebsiteArticle
_, _ = models.Orm.QueryTable(new(models.PlatformWebsiteArticle)).
SetCond(publishedArticleCond()).
Exclude("id__in", ids).
OrderBy("-publish_time", "-id").
Limit(limit - len(pick)).
All(&others, "ID", "CateID", "Title", "Author", "Desc", "Image",
"Top", "Recommend", "Views", "PublishTime")
pick = append(pick, others...)
}
articleIDs := make([]uint64, 0, len(pick))
cateIDs := make([]uint64, 0, len(pick))
for _, r := range pick {
articleIDs = append(articleIDs, r.ID)
if r.CateID > 0 {
cateIDs = append(cateIDs, r.CateID)
}
}
cateMap := models.PlatformWebsiteCategoryMap(cateIDs)
tagMap := models.PlatformWebsiteArticleTagNameMap(articleIDs)
list := make([]map[string]interface{}, 0, len(pick))
for _, r := range pick {
tags := tagMap[r.ID]
if tags == nil {
tags = []string{}
}
list = append(list, articleListItem(r, cateMap[r.CateID], tags))
}
c.ok(map[string]interface{}{"list": list})
}
// NewsView POST /site/news/:id/view — 浏览量 +1(详情页打开时调用)
func (c *IndexWebsiteController) NewsView() {
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.fail(400, 400, "无效的内容ID")
return
}
if _, err = models.Orm.Raw(
"UPDATE yz_platform_website_article SET views = views + 1 WHERE id = ? AND status = ? AND delete_time IS NULL",
id, articleStatusPublished,
).Exec(); err != nil {
c.fail(500, 500, "更新浏览量失败")
return
}
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)
}