更新website

This commit is contained in:
2026-09-17 23:42:41 +08:00
parent 67e819c629
commit 1a4471e34d
18 changed files with 1922 additions and 187 deletions
+96 -3
View File
@@ -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)
}
+8 -3
View File
@@ -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)
+426
View File
@@ -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)
}