增加通知公告功能

This commit is contained in:
hero920103 committed 2026-09-08 23:22:49 +08:00
1 parent beaeb4a234
commit 672c22d9b6
12 files changed
+1416 -6

No files matched your search

+416
View File
@@ -0,0 +1,416 @@
package controllers
import (
"encoding/json"
"io"
"strconv"
"strings"
"time"
"unicode/utf8"
"server/models"
"server/pkg/jwtutil"
"github.com/beego/beego/v2/client/orm"
beego "github.com/beego/beego/v2/server/web"
)
// BackendOaNoticeController OA通知公告管理。
// 数据按租户 tid 隔离;发布后对租户内所有后台用户可见。
type BackendOaNoticeController struct {
beego.Controller
}
// Prepare 所有接口执行前确保通知公告表存在。
func (c *BackendOaNoticeController) Prepare() {
_ = models.EnsureOaNoticeTable()
}
func (c *BackendOaNoticeController) oaNoticeClaims() (*jwtutil.Claims, error) {
auth := c.Ctx.Request.Header.Get("Authorization")
if auth == "" {
return nil, orm.ErrNoRows
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return nil, orm.ErrNoRows
}
claims, err := jwtutil.ParseToken(parts[1])
if err != nil {
return nil, err
}
if claims.UserType != "backend" {
return nil, orm.ErrNoRows
}
return claims, nil
}
func (c *BackendOaNoticeController) ontJsonErr(httpStatus, bizCode int, msg string) {
c.Ctx.Output.SetStatus(httpStatus)
c.Data["json"] = map[string]interface{}{"code": bizCode, "msg": msg}
_ = c.ServeJSON()
}
func (c *BackendOaNoticeController) ontOk(data interface{}) {
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": data}
_ = c.ServeJSON()
}
type oaNoticePayload struct {
Title string `json:"title"`
Content string `json:"content"`
NoticeType int8 `json:"notice_type"`
Level int8 `json:"level"`
IsTop int8 `json:"is_top"`
// Publish 为 true 时保存即发布
Publish bool `json:"publish"`
}
// parseOaNoticePayload 读取并校验公告请求体;失败时直接输出错误响应。
func (c *BackendOaNoticeController) parseOaNoticePayload() (oaNoticePayload, bool) {
var payload oaNoticePayload
raw, err := io.ReadAll(c.Ctx.Request.Body)
if err != nil || json.Unmarshal(raw, &payload) != nil {
c.ontJsonErr(400, 400, "参数错误")
return payload, false
}
payload.Title = strings.TrimSpace(payload.Title)
if payload.Title == "" {
c.ontJsonErr(400, 400, "请输入公告标题")
return payload, false
}
if utf8.RuneCountInString(payload.Title) > 100 {
c.ontJsonErr(400, 400, "公告标题不能超过100字")
return payload, false
}
if payload.NoticeType < 0 || payload.NoticeType > 2 {
payload.NoticeType = 0
}
if payload.Level < 0 || payload.Level > 2 {
payload.Level = 0
}
if payload.IsTop != 1 {
payload.IsTop = 0
}
payload.Content = strings.TrimSpace(payload.Content)
return payload, true
}
// noticeBase 租户内未删除公告的基础查询集
func noticeBase(tid int) orm.QuerySeter {
return models.Orm.QueryTable(new(models.OaNotice)).
Filter("is_deleted", 0).
Filter("tid", tid)
}
// List GET /backend/oa/notice/list
// 管理端分页列表,支持 keyword/status/notice_type 筛选;置顶优先,再按发布时间倒序。
func (c *BackendOaNoticeController) List() {
claims, err := c.oaNoticeClaims()
if err != nil {
c.ontJsonErr(401, 401, "未登录或无权限")
return
}
page, _ := c.GetInt("page", 1)
pageSize, _ := c.GetInt("pageSize", 20)
keyword := strings.TrimSpace(c.GetString("keyword"))
status := strings.TrimSpace(c.GetString("status"))
noticeType := strings.TrimSpace(c.GetString("notice_type"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 500 {
pageSize = 20
}
qs := noticeBase(claims.TenantId)
if keyword != "" {
qs = qs.Filter("title__icontains", keyword)
}
if status == "0" || status == "1" || status == "2" {
v, _ := strconv.Atoi(status)
qs = qs.Filter("status", int8(v))
}
if noticeType == "0" || noticeType == "1" || noticeType == "2" {
v, _ := strconv.Atoi(noticeType)
qs = qs.Filter("notice_type", int8(v))
}
total, _ := qs.Count()
var list []models.OaNotice
_, err = qs.OrderBy("-is_top", "-publish_time", "-id").
Limit(pageSize).Offset((page - 1) * pageSize).
All(&list)
if err != nil && err != orm.ErrNoRows {
c.ontJsonErr(500, 500, "查询失败")
return
}
if list == nil {
list = []models.OaNotice{}
}
c.ontOk(map[string]interface{}{"list": list, "total": total})
}
// Portal GET /backend/oa/notice/portal
// 工作台/仪表盘用:只返回已发布的公告,置顶优先,取最近 limit 条(默认 5,最多 20)。
func (c *BackendOaNoticeController) Portal() {
claims, err := c.oaNoticeClaims()
if err != nil {
c.ontJsonErr(401, 401, "未登录或无权限")
return
}
limit, _ := c.GetInt("limit", 5)
if limit < 1 || limit > 20 {
limit = 5
}
var list []models.OaNotice
_, err = noticeBase(claims.TenantId).
Filter("status", 1).
OrderBy("-is_top", "-publish_time", "-id").
Limit(limit).
All(&list)
if err != nil && err != orm.ErrNoRows {
c.ontJsonErr(500, 500, "查询失败")
return
}
if list == nil {
list = []models.OaNotice{}
}
c.ontOk(map[string]interface{}{"list": list})
}
// Detail GET /backend/oa/notice/detail/:id
// 公告详情,同时累加阅读次数。
func (c *BackendOaNoticeController) Detail() {
claims, err := c.oaNoticeClaims()
if err != nil {
c.ontJsonErr(401, 401, "未登录或无权限")
return
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.ontJsonErr(400, 400, "无效ID")
return
}
var item models.OaNotice
err = noticeBase(claims.TenantId).Filter("id", id).One(&item)
if err != nil {
c.ontJsonErr(404, 404, "公告不存在")
return
}
// 阅读数自增(并发下允许略有偏差,仅为展示用)
_, _ = models.Orm.Raw(
"UPDATE yz_backend_oa_notice SET read_count = read_count + 1 WHERE id = ?", id).Exec()
item.ReadCount++
c.ontOk(item)
}
// Create POST /backend/oa/notice/create
func (c *BackendOaNoticeController) Create() {
claims, err := c.oaNoticeClaims()
if err != nil {
c.ontJsonErr(401, 401, "未登录或无权限")
return
}
payload, ok := c.parseOaNoticePayload()
if !ok {
return
}
now := time.Now()
item := &models.OaNotice{
Tid: claims.TenantId,
Title: payload.Title,
Content: payload.Content,
NoticeType: payload.NoticeType,
Level: payload.Level,
IsTop: payload.IsTop,
Status: 0,
PublisherID: uint64(claims.UserID),
PublisherName: claims.Username,
IsDeleted: 0,
CreateTime: now,
UpdateTime: &now,
}
if payload.Publish {
item.Status = 1
item.PublishTime = &now
}
if _, err := models.Orm.Insert(item); err != nil {
c.ontJsonErr(500, 500, "保存失败")
return
}
c.ontOk(item)
}
// Update POST /backend/oa/notice/update/:id
// 编辑公告内容;已发布的公告编辑后保持发布状态(不改变发布时间)。
func (c *BackendOaNoticeController) Update() {
claims, err := c.oaNoticeClaims()
if err != nil {
c.ontJsonErr(401, 401, "未登录或无权限")
return
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.ontJsonErr(400, 400, "无效ID")
return
}
payload, ok := c.parseOaNoticePayload()
if !ok {
return
}
var item models.OaNotice
err = noticeBase(claims.TenantId).Filter("id", id).One(&item)
if err != nil {
c.ontJsonErr(404, 404, "公告不存在")
return
}
now := time.Now()
item.Title = payload.Title
item.Content = payload.Content
item.NoticeType = payload.NoticeType
item.Level = payload.Level
item.IsTop = payload.IsTop
item.UpdateTime = &now
fields := []string{"Title", "Content", "NoticeType", "Level", "IsTop", "UpdateTime"}
// 草稿保存时选择"发布"则直接发布
if payload.Publish && item.Status != 1 {
item.Status = 1
item.PublishTime = &now
fields = append(fields, "Status", "PublishTime")
}
if _, err := models.Orm.Update(&item, fields...); err != nil {
c.ontJsonErr(500, 500, "保存失败")
return
}
c.ontOk(item)
}
// Delete DELETE /backend/oa/notice/delete/:id 软删除。
func (c *BackendOaNoticeController) Delete() {
claims, err := c.oaNoticeClaims()
if err != nil {
c.ontJsonErr(401, 401, "未登录或无权限")
return
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.ontJsonErr(400, 400, "无效ID")
return
}
now := time.Now()
n, err := noticeBase(claims.TenantId).Filter("id", id).
Update(map[string]interface{}{
"IsDeleted": int8(1),
"DeleteTime": now,
"UpdateTime": now,
})
if err != nil || n == 0 {
c.ontJsonErr(404, 404, "公告不存在或已删除")
return
}
c.ontOk(nil)
}
// Publish POST /backend/oa/notice/publish/:id
// 发布 / 下架切换:草稿、已下架 -> 已发布(写入发布时间);已发布 -> 已下架。
func (c *BackendOaNoticeController) Publish() {
claims, err := c.oaNoticeClaims()
if err != nil {
c.ontJsonErr(401, 401, "未登录或无权限")
return
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.ontJsonErr(400, 400, "无效ID")
return
}
var item models.OaNotice
err = noticeBase(claims.TenantId).Filter("id", id).One(&item)
if err != nil {
c.ontJsonErr(404, 404, "公告不存在")
return
}
now := time.Now()
if item.Status == 1 {
item.Status = 2
item.PublishTime = nil
} else {
item.Status = 1
item.PublishTime = &now
}
item.UpdateTime = &now
if _, err := models.Orm.Update(&item, "Status", "PublishTime", "UpdateTime"); err != nil {
c.ontJsonErr(500, 500, "操作失败")
return
}
c.ontOk(map[string]interface{}{"status": item.Status})
}
// Top POST /backend/oa/notice/top/:id 置顶 / 取消置顶切换。
func (c *BackendOaNoticeController) Top() {
claims, err := c.oaNoticeClaims()
if err != nil {
c.ontJsonErr(401, 401, "未登录或无权限")
return
}
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
if err != nil || id == 0 {
c.ontJsonErr(400, 400, "无效ID")
return
}
var item models.OaNotice
err = noticeBase(claims.TenantId).Filter("id", id).One(&item)
if err != nil {
c.ontJsonErr(404, 404, "公告不存在")
return
}
now := time.Now()
if item.IsTop == 1 {
item.IsTop = 0
} else {
item.IsTop = 1
}
item.UpdateTime = &now
if _, err := models.Orm.Update(&item, "IsTop", "UpdateTime"); err != nil {
c.ontJsonErr(500, 500, "操作失败")
return
}
c.ontOk(map[string]interface{}{"is_top": item.IsTop})
}
BIN
View File
Binary file not shown.
+1
View File
@@ -113,6 +113,7 @@ func Init(_ string) {
new(BackendScheduleReminderSendLog),
new(OaSchedule),
new(OaNotice),
new(BackendOaCompensationScheme),
new(BackendOaPayroll),
new(BackendOaPayrollItem),
+69
View File
@@ -0,0 +1,69 @@
package models
import (
"sync"
"time"
)
// OaNotice OA通知公告: yz_backend_oa_notice
// 按租户 tid 隔离:租户内发布,租户内所有后台用户可见。
// status: 0-草稿 1-已发布 2-已下架;notice_type: 0-通知 1-公告 2-活动;
// level: 0-普通 1-重要 2-紧急;is_top: 0-否 1-是(置顶优先展示)。
type OaNotice struct {
ID uint64 `orm:"column(id);pk;auto" json:"id"`
Tid int `orm:"column(tid)" json:"tid"`
Title string `orm:"column(title);size(255)" json:"title"`
Content string `orm:"column(content);type(text);null" json:"content"`
NoticeType int8 `orm:"column(notice_type);default(0)" json:"notice_type"`
Level int8 `orm:"column(level);default(0)" json:"level"`
Status int8 `orm:"column(status);default(0)" json:"status"`
IsTop int8 `orm:"column(is_top);default(0)" json:"is_top"`
PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"`
PublisherID uint64 `orm:"column(publisher_id);default(0)" json:"publisher_id"`
PublisherName string `orm:"column(publisher_name);size(100);default()" json:"publisher_name"`
ReadCount int `orm:"column(read_count);default(0)" json:"read_count"`
IsDeleted int8 `orm:"column(is_deleted);default(0)" json:"is_deleted"`
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
UpdateTime *time.Time `orm:"column(update_time);auto_now;type(datetime);null" json:"update_time"`
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
}
func (m *OaNotice) TableName() string {
return "yz_backend_oa_notice"
}
var oaNoticeTableOnce sync.Once
// EnsureOaNoticeTable 首次访问通知公告接口时自动建表(若不存在)。
// 与日程表保持一致的"运行时自愈"策略,避免新功能上线还要手工执行建表脚本。
func EnsureOaNoticeTable() error {
if Orm == nil {
return nil
}
var err error
oaNoticeTableOnce.Do(func() {
_, err = Orm.Raw(`
CREATE TABLE IF NOT EXISTS yz_backend_oa_notice (
id bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tid int NOT NULL DEFAULT 0 COMMENT '租户ID',
title varchar(255) NOT NULL DEFAULT '' COMMENT '公告标题',
content text COMMENT '公告内容',
notice_type tinyint NOT NULL DEFAULT 0 COMMENT '类型 0-通知 1-公告 2-活动',
level tinyint NOT NULL DEFAULT 0 COMMENT '紧急程度 0-普通 1-重要 2-紧急',
status tinyint NOT NULL DEFAULT 0 COMMENT '状态 0-草稿 1-已发布 2-已下架',
is_top tinyint NOT NULL DEFAULT 0 COMMENT '是否置顶 0-否 1-是',
publish_time datetime DEFAULT NULL COMMENT '发布时间',
publisher_id bigint unsigned NOT NULL DEFAULT 0 COMMENT '发布人ID',
publisher_name varchar(100) NOT NULL DEFAULT '' COMMENT '发布人姓名',
read_count int NOT NULL DEFAULT 0 COMMENT '阅读次数',
is_deleted tinyint NOT NULL DEFAULT 0 COMMENT '是否删除 0-否 1-是',
create_time datetime DEFAULT NULL COMMENT '创建时间',
update_time datetime DEFAULT NULL COMMENT '更新时间',
delete_time datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (id),
KEY idx_tid_status (tid, status),
KEY idx_tid_top (tid, is_top)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='OA通知公告表'`).Exec()
})
return err
}
+10
View File
@@ -268,6 +268,16 @@ func RegisterAuthRoutes() {
beego.Router("/backend/oa/schedule/finish/:id", &controllers.BackendOaScheduleController{}, "post:FinishSchedule")
beego.Router("/backend/oa/schedule/carry/:id", &controllers.BackendOaScheduleController{}, "post:CarrySchedule")
beego.Router("/backend/oa/schedule/carry-pending", &controllers.BackendOaScheduleController{}, "post:CarryPending")
// OA通知公告
beego.Router("/backend/oa/notice/list", &controllers.BackendOaNoticeController{}, "get:List")
beego.Router("/backend/oa/notice/portal", &controllers.BackendOaNoticeController{}, "get:Portal")
beego.Router("/backend/oa/notice/detail/:id", &controllers.BackendOaNoticeController{}, "get:Detail")
beego.Router("/backend/oa/notice/create", &controllers.BackendOaNoticeController{}, "post:Create")
beego.Router("/backend/oa/notice/update/:id", &controllers.BackendOaNoticeController{}, "post:Update")
beego.Router("/backend/oa/notice/delete/:id", &controllers.BackendOaNoticeController{}, "delete:Delete")
beego.Router("/backend/oa/notice/publish/:id", &controllers.BackendOaNoticeController{}, "post:Publish")
beego.Router("/backend/oa/notice/top/:id", &controllers.BackendOaNoticeController{}, "post:Top")
}
// registerOrganizationRoutes 为指定模块前缀注册组织架构路由。